├── .babelrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── NOTICE
├── README.md
├── Vagrantfile
├── bootstrap.sh
├── credentialstore_keygen.py
├── email_cron.py
├── manage.py
├── package-lock.json
├── package.json
├── ratelimit_cron.py
├── requirements.txt
├── rollback_worker.py
├── runtests.py
├── stats_cron.py
├── sync_cpulimit.py
├── sync_global_watchdog.py
├── sync_poll_triggers.py
├── sync_remote_triggers.py
├── sync_scheduler.py
├── sync_watchdog.py
├── sync_worker.py
├── tapiriik
├── __init__.py
├── auth
│ ├── __init__.py
│ ├── credential_storage.py
│ └── totp.py
├── database
│ ├── __init__.py
│ └── tz.py
├── frontend
│ ├── app.jsx
│ └── components
│ │ ├── aerobiaConfig.jsx
│ │ ├── csrftoken.jsx
│ │ ├── empty.jsx
│ │ ├── exportConfig.jsx
│ │ ├── localExporter
│ │ └── localExporterConfig.jsx
│ │ ├── ruleLine.jsx
│ │ └── ruleList.jsx
├── local_settings.py.example
├── locale
│ ├── fr
│ │ └── LC_MESSAGES
│ │ │ ├── django.mo
│ │ │ └── django.po
│ └── ru
│ │ └── LC_MESSAGES
│ │ ├── django.mo
│ │ └── django.po
├── messagequeue
│ └── __init__.py
├── payments
│ ├── __init__.py
│ ├── external
│ │ ├── __init__.py
│ │ ├── motivato.py
│ │ └── provider_base.py
│ └── payments.py
├── requests_lib.py
├── services
│ ├── Aerobia
│ │ ├── __init__.py
│ │ └── aerobia.py
│ ├── BeginnerTriathlete
│ │ ├── __init__.py
│ │ └── beginnertriathlete.py
│ ├── DecathlonCoach
│ │ ├── __init__.py
│ │ └── decathloncoach.py
│ ├── Dropbox
│ │ ├── __init__.py
│ │ └── dropbox.py
│ ├── Endomondo
│ │ ├── __init__.py
│ │ └── endomondo.py
│ ├── GarminConnect
│ │ ├── __init__.py
│ │ └── garminconnect.py
│ ├── LocalExporter
│ │ ├── __init__.py
│ │ └── localExporter.py
│ ├── Motivato
│ │ ├── __init__.py
│ │ └── motivato.py
│ ├── NikePlus
│ │ ├── __init__.py
│ │ └── nikeplus.py
│ ├── PolarFlow
│ │ ├── __init__.py
│ │ └── polarflow.py
│ ├── PolarPersonalTrainer
│ │ ├── __init__.py
│ │ ├── polarpersonaltrainer.py
│ │ └── pptToTcx.py
│ ├── Pulsstory
│ │ ├── __init__.py
│ │ └── pulsstory.py
│ ├── RideWithGPS
│ │ ├── __init__.py
│ │ └── rwgps.py
│ ├── RunKeeper
│ │ ├── __init__.py
│ │ └── runkeeper.py
│ ├── Setio
│ │ ├── __init__.py
│ │ └── setio.py
│ ├── Singletracker
│ │ ├── __init__.py
│ │ └── singletracker.py
│ ├── Smashrun
│ │ ├── __init__.py
│ │ └── smashrun.py
│ ├── SportTracks
│ │ ├── __init__.py
│ │ └── sporttracks.py
│ ├── Strava
│ │ ├── __init__.py
│ │ └── strava.py
│ ├── TrainAsONE
│ │ ├── __init__.py
│ │ └── trainasone.py
│ ├── TrainerRoad
│ │ ├── __init__.py
│ │ └── trainerroad.py
│ ├── TrainingPeaks
│ │ ├── __init__.py
│ │ └── trainingpeaks.py
│ ├── VeloHero
│ │ ├── __init__.py
│ │ └── velohero.py
│ ├── __init__.py
│ ├── api.py
│ ├── auto_pause.py
│ ├── devices.py
│ ├── exception_tools.py
│ ├── fit.py
│ ├── gpx.py
│ ├── interchange.py
│ ├── pwx.py
│ ├── ratelimiting.py
│ ├── rollback.py
│ ├── service.py
│ ├── service_base.py
│ ├── service_record.py
│ ├── sessioncache.py
│ ├── statistic_calculator.py
│ ├── stream_sampling.py
│ └── tcx.py
├── settings.py
├── sync
│ ├── __init__.py
│ ├── activity_record.py
│ └── sync.py
├── testing
│ ├── __init__.py
│ ├── data
│ │ ├── garmin_parse_1.tcx
│ │ └── test1.tcx
│ ├── gpx.py
│ ├── interchange.py
│ ├── statistics.py
│ ├── sync.py
│ ├── tcx.py
│ └── testtools.py
├── urls.py
├── web
│ ├── __init__.py
│ ├── context_processors.py
│ ├── email.py
│ ├── models.py
│ ├── startup.py
│ ├── static
│ │ ├── css
│ │ │ ├── diagnostics.css
│ │ │ └── style.css
│ │ ├── errors
│ │ │ └── upgrade.html
│ │ ├── img
│ │ │ ├── android-chrome-192x192.png
│ │ │ ├── android-chrome-512x512.png
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── art
│ │ │ │ └── mapmyfitness-logo.ai
│ │ │ ├── bg-sunset.svg
│ │ │ ├── connector-dot.png
│ │ │ ├── email
│ │ │ │ ├── corner_bl.gif
│ │ │ │ ├── corner_br.gif
│ │ │ │ ├── corner_tl.gif
│ │ │ │ ├── corner_tr.gif
│ │ │ │ ├── tapiriik-wordmark.gif
│ │ │ │ └── trans.gif
│ │ │ ├── favicon-16x16.png
│ │ │ ├── favicon-32x32.png
│ │ │ ├── favicon.ico
│ │ │ ├── favicon.png
│ │ │ ├── garminconnect_useradd.png
│ │ │ ├── link.png
│ │ │ ├── mstile-150x150.png
│ │ │ ├── pp-logo.png
│ │ │ ├── privacy-cached.png
│ │ │ ├── privacy-no.png
│ │ │ ├── privacy-optin.png
│ │ │ ├── privacy-yes.png
│ │ │ ├── rule-add.svg
│ │ │ ├── rule-delete.svg
│ │ │ ├── safari-pinned-tab.svg
│ │ │ ├── services
│ │ │ │ ├── aerobia.png
│ │ │ │ ├── aerobia_l.png
│ │ │ │ ├── beginnertriathlete.png
│ │ │ │ ├── beginnertriathlete_l.png
│ │ │ │ ├── decathloncoach.png
│ │ │ │ ├── decathloncoach_l.png
│ │ │ │ ├── dropbox.png
│ │ │ │ ├── dropbox_l.png
│ │ │ │ ├── endomondo.png
│ │ │ │ ├── endomondo_l.png
│ │ │ │ ├── garminconnect.png
│ │ │ │ ├── garminconnect_l.png
│ │ │ │ ├── localexporter.png
│ │ │ │ ├── localexporter_l.png
│ │ │ │ ├── motivato.png
│ │ │ │ ├── motivato_l.png
│ │ │ │ ├── nikeplus.png
│ │ │ │ ├── nikeplus_l.png
│ │ │ │ ├── polarflow.png
│ │ │ │ ├── polarflow_l.png
│ │ │ │ ├── polarpersonaltrainer.png
│ │ │ │ ├── polarpersonaltrainer_l.png
│ │ │ │ ├── pulsstory.png
│ │ │ │ ├── pulsstory_l.png
│ │ │ │ ├── runkeeper.png
│ │ │ │ ├── runkeeper_l.png
│ │ │ │ ├── runsense.png
│ │ │ │ ├── runsense_l.png
│ │ │ │ ├── rwgps.png
│ │ │ │ ├── rwgps_l.png
│ │ │ │ ├── setio.png
│ │ │ │ ├── setio_l.png
│ │ │ │ ├── singletracker.png
│ │ │ │ ├── singletracker_l.png
│ │ │ │ ├── smashrun.png
│ │ │ │ ├── smashrun_l.png
│ │ │ │ ├── sporttracks.png
│ │ │ │ ├── sporttracks_l.png
│ │ │ │ ├── strava.png
│ │ │ │ ├── strava_l.png
│ │ │ │ ├── trainasone.png
│ │ │ │ ├── trainasone_l.png
│ │ │ │ ├── trainerroad.png
│ │ │ │ ├── trainerroad_l.png
│ │ │ │ ├── trainingpeaks.png
│ │ │ │ ├── trainingpeaks_l.png
│ │ │ │ ├── velohero.png
│ │ │ │ └── velohero_l.png
│ │ │ ├── snow.png
│ │ │ ├── starfield.svg
│ │ │ ├── sync-arrow.png
│ │ │ ├── sync-fail.png
│ │ │ ├── sync-go.png
│ │ │ ├── sync-info.png
│ │ │ ├── sync-ok.png
│ │ │ ├── sync-pause.png
│ │ │ ├── sync-reauth.png
│ │ │ ├── sync-settings.png
│ │ │ ├── sync-spin.png
│ │ │ ├── tapiriik-arabic.png
│ │ │ ├── tapiriik-hebrew.png
│ │ │ ├── tapiriik-hindi.png
│ │ │ ├── tapiriik-inuktitut.png
│ │ │ ├── tapiriik-punjabi.png
│ │ │ ├── trainingpeaks_download
│ │ │ └── unlink.png
│ │ ├── js
│ │ │ ├── Chart.min.js
│ │ │ ├── bundles
│ │ │ │ └── exercisyncApp.js
│ │ │ ├── datepicker
│ │ │ │ ├── pikaday-1.6.1.min.css
│ │ │ │ └── pikaday-1.6.1.min.js
│ │ │ ├── jquery.address-1.5.min.js
│ │ │ ├── jstz.min.js
│ │ │ ├── tapiriik-ng.js
│ │ │ └── tapiriik.js
│ │ └── site.webmanifest
│ ├── templates
│ │ ├── activities-dashboard.html
│ │ ├── auth
│ │ │ ├── disconnect.html
│ │ │ └── login.html
│ │ ├── config
│ │ │ ├── aerobia.html
│ │ │ ├── dropbox.html
│ │ │ └── localexporter.html
│ │ ├── dashboard.html
│ │ ├── diag
│ │ │ ├── dashboard.html
│ │ │ ├── error.html
│ │ │ ├── error_error_not_found.html
│ │ │ ├── error_user_not_found.html
│ │ │ ├── errors.html
│ │ │ ├── graphs.html
│ │ │ ├── login.html
│ │ │ ├── payments.html
│ │ │ └── user.html
│ │ ├── donation.html
│ │ ├── download.html
│ │ ├── email
│ │ │ ├── data_download.html
│ │ │ ├── payment_confirm.html
│ │ │ ├── payment_reclaim.html
│ │ │ ├── payment_renew.html
│ │ │ └── template.html
│ │ ├── js-bridge.js
│ │ ├── oauth-failure.html
│ │ ├── oauth-return.html
│ │ ├── payment.html
│ │ ├── payments
│ │ │ ├── claim.html
│ │ │ ├── claim_return_fail.html
│ │ │ ├── confirmed.html
│ │ │ └── return.html
│ │ ├── privacy.html
│ │ ├── recent-sync-activity-block.html
│ │ ├── rollback.html
│ │ ├── service-blockingexception.html
│ │ ├── service-button.html
│ │ ├── settings-block.html
│ │ ├── settings.html
│ │ ├── site-iframe.html
│ │ ├── site.html
│ │ ├── static
│ │ │ ├── contact.html
│ │ │ ├── credits.html
│ │ │ ├── faq.html
│ │ │ ├── garmin_connect_bad_data.html
│ │ │ └── garmin_connect_users.html
│ │ ├── supported-activities.html
│ │ └── supported-services-poll.html
│ ├── templatetags
│ │ ├── __init__.py
│ │ ├── displayutils.py
│ │ ├── services.py
│ │ └── users.py
│ ├── tests.py
│ ├── views.py
│ └── views
│ │ ├── __init__.py
│ │ ├── ab.py
│ │ ├── account.py
│ │ ├── activities_dashboard.py
│ │ ├── aerobia
│ │ └── __init__.py
│ │ ├── auth.py
│ │ ├── config
│ │ └── __init__.py
│ │ ├── dashboard.py
│ │ ├── diagnostics.py
│ │ ├── download.py
│ │ ├── dropbox
│ │ └── __init__.py
│ │ ├── oauth
│ │ └── __init__.py
│ │ ├── payments
│ │ └── __init__.py
│ │ ├── privacy.py
│ │ ├── rollback.py
│ │ ├── settings.py
│ │ ├── supported_activities.py
│ │ ├── supported_services.py
│ │ └── sync.py
└── wsgi.py
├── tz_ingest.py
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "env",
4 | "react",
5 | "stage-2"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.log.*
3 | *.pot
4 | *.pyc
5 | local_settings.py
6 | *.sublime-*
7 | private/
8 | node_modules/
9 | export_data/
10 | webpack-stats.json
11 | .vscode/
12 | .exercisync/
13 | *.hot-update.js
14 | *.hot-update.json
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6"
4 | services:
5 | - mongodb
6 | - redis-server
7 | - rabbitmq
8 | install:
9 | - "pip install -r requirements.txt"
10 | - "cp tapiriik/local_settings.py.example tapiriik/local_settings.py"
11 | script: "python runtests.py"
12 | sudo: false
13 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | exercisync based on tapiriik project under the Apache 2.0 license, https://github.com/cpfair/tapiriik
2 |
3 | exercisync includes some components of Django, an open-source project under the BSD license, the text of which is available at https://github.com/django/django/blob/master/LICENSE
4 |
5 | exercisync requires, but does not include, several other freely available software packages - please refer to requirements.txt
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/Antash/exercisync)
2 |
3 | ## Credits
4 | This project is based on https://github.com/cpfair/tapiriik code by Collin Fair.
5 |
6 | ## Licensing
7 | exercisync is an Apache 2.0 Licensed open-source project.
8 |
9 | ## Translation
10 | To use locale translation, it requires the package gettext.
11 | To launch a new language, you have to generate the locale with : python3 manage.py makemessages -l 'fr'
12 | Secondly, after translation (edition of po files), compile the files : python manage.py compilemessages
13 |
--------------------------------------------------------------------------------
/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Install system requirements
4 |
5 | # mongodb multipolygon geojson support needs at least mongodb 2.6. trusty has 2.4 by default
6 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
7 | echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
8 |
9 |
10 | sudo apt-get update
11 | sudo apt-get install -y python3-pip libxml2-dev libxslt-dev zlib1g-dev git redis-server rabbitmq-server mongodb-org=2.6.9
12 |
13 | # Fix pip
14 | pip3 install --upgrade pip
15 |
16 | # Install app requirements
17 | pip install --upgrade -r /vagrant/requirements.txt
18 |
19 | # Fix the default python instance
20 | update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1
21 | update-alternatives --install /usr/bin/python python /usr/bin/python3.4 2
22 |
23 | # Put in a default local_settings.py (if one doesn't exist)
24 | if [ ! -f /vagrant/tapiriik/local_settings.py ]; then
25 | cp /vagrant/tapiriik/local_settings.py.example /vagrant/tapiriik/local_settings.py
26 | # Generate credential storage keys
27 | python /vagrant/credentialstore_keygen.py >> /vagrant/tapiriik/local_settings.py
28 | fi
29 |
30 |
--------------------------------------------------------------------------------
/credentialstore_keygen.py:
--------------------------------------------------------------------------------
1 | from Crypto.PublicKey import RSA
2 | key = RSA.generate(2048)
3 |
4 | print("CREDENTIAL_STORAGE_PRIVATE_KEY = " + key.exportKey("DER").__repr__())
5 | print("CREDENTIAL_STORAGE_PUBLIC_KEY = " + key.publickey().exportKey("DER").__repr__())
6 |
--------------------------------------------------------------------------------
/email_cron.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db
2 | from tapiriik.web.email import generate_message_from_template, send_email
3 | from tapiriik.services import Service
4 | from tapiriik.settings import WITHDRAWN_SERVICES
5 | from datetime import datetime, timedelta
6 | import os
7 | import math
8 | os.environ["DJANGO_SETTINGS_MODULE"] = "tapiriik.settings"
9 |
10 | # Renewal emails
11 | now = datetime.utcnow()
12 | expiry_window_open = now - timedelta(days=28)
13 | expiry_window_close = now
14 | expired_payments = db.payments.find({"Expiry": {"$gt": expiry_window_open, "$lt": expiry_window_close}, "ReminderEmailSent": {"$ne": True}})
15 |
16 | for expired_payment in expired_payments:
17 | connected_user = db.users.find_one({"Payments._id": expired_payment["_id"]})
18 | print("Composing renewal email for %s" % expired_payment["Email"])
19 | if not connected_user:
20 | print("...no associated user")
21 | continue
22 | connected_services_names = [Service.FromID(x["Service"]).DisplayName for x in connected_user["ConnectedServices"] if x["Service"] not in WITHDRAWN_SERVICES]
23 |
24 | if len(connected_services_names) == 0:
25 | connected_services_names = ["fitness tracking"]
26 | elif len(connected_services_names) == 1:
27 | connected_services_names.append("other fitness tracking")
28 | if len(connected_services_names) > 1:
29 | connected_services_names = ", ".join(connected_services_names[:-1]) + " and " + connected_services_names[-1] + " accounts"
30 | else:
31 | connected_services_names = connected_services_names[0] + " accounts"
32 |
33 | subscription_days = round((expired_payment["Expiry"] - expired_payment["Timestamp"]).total_seconds() / (60 * 60 * 24))
34 | subscription_fuzzy_time_map = {
35 | (0, 8): "few days",
36 | (8, 31): "few weeks",
37 | (31, 150): "few months",
38 | (150, 300): "half a year",
39 | (300, 999): "year"
40 | }
41 | subscription_fuzzy_time = [v for k,v in subscription_fuzzy_time_map.items() if k[0] <= subscription_days and k[1] > subscription_days][0]
42 |
43 | activity_records = db.activity_records.find_one({"UserID": connected_user["_id"]})
44 | total_distance_synced = None
45 | if activity_records:
46 | total_distance_synced = sum([x["Distance"] for x in activity_records["Activities"] if x["Distance"]])
47 | total_distance_synced = math.floor(total_distance_synced/1000 / 100) * 100
48 |
49 | context = {
50 | "account_list": connected_services_names,
51 | "subscription_days": subscription_days,
52 | "subscription_fuzzy_time": subscription_fuzzy_time,
53 | "distance": total_distance_synced
54 | }
55 | message, plaintext_message = generate_message_from_template("email/payment_renew.html", context)
56 | send_email(expired_payment["Email"], "tapiriik automatic synchronization expiry", message, plaintext_message=plaintext_message)
57 | db.payments.update({"_id": expired_payment["_id"]}, {"$set": {"ReminderEmailSent": True}})
58 | print("...sent")
59 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tapiriik.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "exercisync",
3 | "version": "1.0.0",
4 | "description": "exercisync is an Apache 2.0 Licensed open-source project.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "webpack --config webpack.config.js --mode production",
9 | "watch": "webpack --config webpack.config.js --watch --mode development"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/Antash/exercisync.git"
14 | },
15 | "author": "Anton Ashmarin",
16 | "license": "Apache-2.0",
17 | "bugs": {
18 | "url": "https://github.com/Antash/exercisync/issues"
19 | },
20 | "homepage": "https://github.com/Antash/exercisync#readme",
21 | "babel": {
22 | "presets": [
23 | "env",
24 | "react",
25 | "stage-2"
26 | ]
27 | },
28 | "devDependencies": {
29 | "babel": "^6.23.0",
30 | "babel-core": "^6.26.3",
31 | "babel-loader": "^7.1.4",
32 | "babel-preset-env": "^1.6.1",
33 | "babel-preset-react": "^6.24.1",
34 | "babel-preset-stage-2": "^6.24.1",
35 | "css-loader": "^0.28.11",
36 | "react": "^16.3.2",
37 | "react-dom": "^16.3.2",
38 | "react-hot-loader": "^4.1.2",
39 | "style-loader": "^0.21.0",
40 | "webpack": "^4.7.0",
41 | "webpack-bundle-tracker": "^0.3.0",
42 | "webpack-cli": "^3.1.2"
43 | },
44 | "dependencies": {
45 | "react-select": "^1.2.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ratelimit_cron.py:
--------------------------------------------------------------------------------
1 | from tapiriik.services import Service
2 | from tapiriik.services.ratelimiting import RateLimit
3 |
4 | for svc in Service.List():
5 | RateLimit.Refresh(svc.ID, svc.GlobalRateLimits)
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | pymongo==3.0.1
3 | pytz
4 | lxml
5 | dropbox
6 | python-dateutil
7 | Django==1.8.2
8 | pycryptodome
9 | celery
10 | django-pipeline==1.5.1
11 | requests_oauthlib==0.4.0
12 | redis==2.10.6
13 | django-ipware
14 | smashrun-client>=0.6.0
15 | beautifulsoup4
16 | isodate
17 | django-webpack-loader
18 | cssmin
19 | jsmin
--------------------------------------------------------------------------------
/rollback_worker.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db, close_connections
2 | from tapiriik.settings import RABBITMQ_BROKER_URL, MONGO_HOST, MONGO_FULL_WRITE_CONCERN
3 | from tapiriik import settings
4 | from datetime import datetime
5 |
6 | from celery import Celery
7 | from celery.signals import worker_shutdown
8 | from datetime import datetime
9 |
10 | class _celeryConfig:
11 | CELERY_ROUTES = {
12 | "rollback_worker.rollback_task": {"queue": "tapiriik-rollback"}
13 | }
14 | CELERYD_CONCURRENCY = 1
15 | CELERYD_PREFETCH_MULTIPLIER = 1
16 |
17 | celery_app = Celery('rollback_worker', broker=RABBITMQ_BROKER_URL)
18 | celery_app.config_from_object(_celeryConfig())
19 |
20 | @worker_shutdown.connect
21 | def celery_shutdown(**kwargs):
22 | close_connections()
23 |
24 | @celery_app.task()
25 | def rollback_task(task_id):
26 | from tapiriik.services.rollback import RollbackTask
27 | print("Starting rollback task %s" % task_id)
28 | task = RollbackTask.Get(task_id)
29 | task.Execute()
30 |
31 | def schedule_rollback_task(task_id):
32 | rollback_task.apply_async(args=[task_id])
33 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | import tapiriik.database
2 | tapiriik.database.db = tapiriik.database._connection["tapiriik_test"]
3 | tapiriik.database.cachedb = tapiriik.database._connection["tapiriik_cache_test"]
4 |
5 | from tapiriik.testing import *
6 | import unittest
7 | unittest.main()
8 |
9 | tapiriik.database._connection.drop_database("tapiriik_test")
10 | tapiriik.database._connection.drop_database("tapiriik_cache_test")
11 |
--------------------------------------------------------------------------------
/sync_cpulimit.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import subprocess
4 |
5 | cpulimit_procs = {}
6 | worker_cpu_limit = int(os.environ.get("TAPIRIIK_WORKER_CPU_LIMIT", 4))
7 |
8 | while True:
9 | active_pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] # Sorry, operating systems without procfs
10 | for pid in active_pids:
11 | try:
12 | proc_cmd = open("/proc/%s/cmdline" % pid, "r").read()
13 | except IOError:
14 | continue
15 | else:
16 | if "sync_worker.py" in proc_cmd:
17 | if pid not in cpulimit_procs or cpulimit_procs[pid].poll():
18 | cpulimit_procs[pid] = subprocess.Popen(["cpulimit", "-l", str(worker_cpu_limit), "-p", pid])
19 |
20 | for k in list(cpulimit_procs.keys()):
21 | if cpulimit_procs[k].poll():
22 | cpulimit_procs[k].wait()
23 | del cpulimit_procs[k]
24 |
25 | time.sleep(1)
26 |
--------------------------------------------------------------------------------
/sync_global_watchdog.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db, close_connections
2 | from datetime import datetime, timedelta
3 | # I resisted calling this file sync_watchdog_watchdog.py, but that's what it is.
4 | # Normally a watchdog process runs on each server and detects hung/crashed
5 | # synchronization tasks, returning them to the queue for another worker to pick
6 | # up. Except, if the entire server goes down, the watchdog no longer runs and
7 | # users get stuck. So, we need a watchdog for the watchdogs. A separate process
8 | # reschedules users left stranded by a failed server/process.
9 |
10 | SERVER_WATCHDOG_TIMEOUT = timedelta(minutes=5)
11 |
12 | print("Global sync watchdog run at %s" % datetime.now())
13 |
14 | for host_record in db.sync_watchdogs.find():
15 | if datetime.utcnow() - host_record["Timestamp"] > SERVER_WATCHDOG_TIMEOUT:
16 | print("Releasing users held by %s (last check-in %s)" % (host_record["Host"], host_record["Timestamp"]))
17 | db.users.update({"SynchronizationHost": host_record["Host"]}, {"$unset": {"SynchronizationWorker": True}}, multi=True)
18 | db.sync_workers.remove({"Host": host_record["Host"]}, multi=True)
19 | db.sync_watchdogs.remove({"_id": host_record["_id"]})
20 |
21 | close_connections()
22 |
--------------------------------------------------------------------------------
/sync_poll_triggers.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db, close_connections
2 | from tapiriik.requests_lib import patch_requests_source_address
3 | from tapiriik.settings import RABBITMQ_BROKER_URL, MONGO_HOST, MONGO_FULL_WRITE_CONCERN
4 | from tapiriik import settings
5 | from datetime import datetime
6 |
7 | if isinstance(settings.HTTP_SOURCE_ADDR, list):
8 | settings.HTTP_SOURCE_ADDR = settings.HTTP_SOURCE_ADDR[0]
9 | patch_requests_source_address((settings.HTTP_SOURCE_ADDR, 0))
10 |
11 | from tapiriik.services import Service
12 | from celery import Celery
13 | from celery.signals import worker_shutdown
14 | from datetime import datetime
15 |
16 | class _celeryConfig:
17 | CELERY_ROUTES = {
18 | "sync_poll_triggers.trigger_poll": {"queue": "tapiriik-poll"}
19 | }
20 | CELERYD_CONCURRENCY = 1 # Otherwise the GC rate limiting breaks since file locking is per-process.
21 | CELERYD_PREFETCH_MULTIPLIER = 1 # The message queue could use some exercise.
22 |
23 | celery_app = Celery('sync_poll_triggers', broker=RABBITMQ_BROKER_URL)
24 | celery_app.config_from_object(_celeryConfig())
25 |
26 | @worker_shutdown.connect
27 | def celery_shutdown(**kwargs):
28 | close_connections()
29 |
30 | @celery_app.task(acks_late=True)
31 | def trigger_poll(service_id, index):
32 | from tapiriik.auth import User
33 | print("Polling %s-%d" % (service_id, index))
34 | svc = Service.FromID(service_id)
35 | affected_connection_external_ids = svc.PollPartialSyncTrigger(index)
36 | print("Triggering %d connections via %s-%d" % (len(affected_connection_external_ids), service_id, index))
37 |
38 | # MONGO_FULL_WRITE_CONCERN because there was a race where users would get picked for synchronization before their service record was updated on the correct secondary
39 | # So it'd think the service wasn't triggered
40 | db.connections.update({"Service": service_id, "ExternalID": {"$in": affected_connection_external_ids}}, {"$set":{"TriggerPartialSync": True, "TriggerPartialSyncTimestamp": datetime.utcnow()}}, multi=True, w=MONGO_FULL_WRITE_CONCERN)
41 |
42 | affected_connection_ids = db.connections.find({"Service": svc.ID, "ExternalID": {"$in": affected_connection_external_ids}}, {"_id": 1})
43 | affected_connection_ids = [x["_id"] for x in affected_connection_ids]
44 | trigger_users_query = User.PaidUserMongoQuery()
45 | trigger_users_query.update({"ConnectedServices.ID": {"$in": affected_connection_ids}})
46 | trigger_users_query.update({"Config.suppress_auto_sync": {"$ne": True}})
47 | db.users.update(trigger_users_query, {"$set": {"NextSynchronization": datetime.utcnow()}}, multi=True) # It would be nicer to use the Sync.Schedule... method, but I want to cleanly do this in bulk
48 |
49 | db.poll_stats.insert({"Service": service_id, "Index": index, "Timestamp": datetime.utcnow(), "TriggerCount": len(affected_connection_external_ids)})
50 |
51 | def schedule_trigger_poll():
52 | schedule_data = list(db.trigger_poll_scheduling.find())
53 | print("Scheduler run at %s" % datetime.now())
54 | for svc in Service.List():
55 | if svc.PartialSyncTriggerRequiresPolling:
56 | print("Checking %s's %d poll indexes" % (svc.ID, svc.PartialSyncTriggerPollMultiple))
57 | for idx in range(svc.PartialSyncTriggerPollMultiple):
58 | svc_schedule = [x for x in schedule_data if x["Service"] == svc.ID and x["Index"] == idx]
59 | if not svc_schedule:
60 | svc_schedule = {"Service": svc.ID, "Index": idx, "LastScheduled": datetime.min}
61 | else:
62 | svc_schedule = svc_schedule[0]
63 |
64 | if datetime.utcnow() - svc_schedule["LastScheduled"] > svc.PartialSyncTriggerPollInterval:
65 | print("Scheduling %s-%d" % (svc.ID, idx))
66 | svc_schedule["LastScheduled"] = datetime.utcnow()
67 | trigger_poll.apply_async(args=[svc.ID, idx], expires=svc.PartialSyncTriggerPollInterval.total_seconds(), time_limit=svc.PartialSyncTriggerPollInterval.total_seconds())
68 | db.trigger_poll_scheduling.update({"Service": svc.ID, "Index": idx}, svc_schedule, upsert=True)
69 |
70 | if __name__ == "__main__":
71 | schedule_trigger_poll()
72 | close_connections()
--------------------------------------------------------------------------------
/sync_remote_triggers.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db, close_connections
2 | from tapiriik.settings import RABBITMQ_BROKER_URL, MONGO_FULL_WRITE_CONCERN
3 | from datetime import datetime
4 | from celery import Celery
5 | from celery.signals import worker_shutdown
6 |
7 | class _celeryConfig:
8 | CELERY_ROUTES = {
9 | "sync_remote_triggers.trigger_remote": {"queue": "tapiriik-remote-trigger"}
10 | }
11 | CELERYD_CONCURRENCY = 1 # Otherwise the GC rate limiting breaks since file locking is per-process.
12 | CELERYD_PREFETCH_MULTIPLIER = 1 # The message queue could use some exercise.
13 |
14 | celery_app = Celery('sync_remote_triggers', broker=RABBITMQ_BROKER_URL)
15 | celery_app.config_from_object(_celeryConfig())
16 |
17 | @worker_shutdown.connect
18 | def celery_shutdown(**kwargs):
19 | close_connections()
20 |
21 | @celery_app.task(acks_late=True)
22 | def trigger_remote(service_id, affected_connection_external_ids_with_payloads):
23 | from tapiriik.auth import User
24 | from tapiriik.services import Service
25 | svc = Service.FromID(service_id)
26 | affected_connection_ids = list()
27 |
28 | for item in affected_connection_external_ids_with_payloads:
29 | if isinstance(item, list):
30 | external_id, payload = item
31 | else:
32 | external_id = item
33 | payload = None
34 | update_connection_query = {"$set":{"TriggerPartialSync": True, "TriggerPartialSyncTimestamp": datetime.utcnow()}}
35 | if payload is not None:
36 | update_connection_query.update({"$push": {"TriggerPartialSyncPayloads": payload, "$slice": -90}})
37 | record = db.connections.find_and_modify({"Service": svc.ID, "ExternalID": external_id}, update_connection_query, w=MONGO_FULL_WRITE_CONCERN)
38 | if record is not None:
39 | affected_connection_ids.append(record["_id"])
40 |
41 | trigger_users_query = User.PaidUserMongoQuery()
42 | trigger_users_query.update({"ConnectedServices.ID": {"$in": affected_connection_ids}})
43 | trigger_users_query.update({"Config.suppress_auto_sync": {"$ne": True}})
44 | db.users.update(trigger_users_query, {"$set": {"NextSynchronization": datetime.utcnow()}}, multi=True) # It would be nicer to use the Sync.Schedule... method, but I want to cleanly do this in bulk
45 |
--------------------------------------------------------------------------------
/sync_scheduler.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db
2 | from tapiriik.sync import Sync
3 | from datetime import datetime
4 | from pymongo.read_preferences import ReadPreference
5 | import kombu
6 | import time
7 | import uuid
8 |
9 | Sync.InitializeWorkerBindings()
10 |
11 | producer = kombu.Producer(Sync._channel, Sync._exchange)
12 |
13 | while True:
14 | generation = str(uuid.uuid4())
15 | queueing_at = datetime.utcnow()
16 | users = list(db.users.with_options(read_preference=ReadPreference.PRIMARY).find(
17 | {
18 | "NextSynchronization": {"$lte": datetime.utcnow()},
19 | "QueuedAt": {"$exists": False}
20 | },
21 | {
22 | "_id": True,
23 | "SynchronizationHostRestriction": True
24 | }
25 | ))
26 | scheduled_ids = [x["_id"] for x in users]
27 | print("Found %d users at %s" % (len(scheduled_ids), datetime.utcnow()))
28 | db.users.update({"_id": {"$in": scheduled_ids}}, {"$set": {"QueuedAt": queueing_at, "QueuedGeneration": generation}, "$unset": {"NextSynchronization": True}}, multi=True)
29 | print("Marked %d users as queued at %s" % (len(scheduled_ids), datetime.utcnow()))
30 | for user in users:
31 | producer.publish({"user_id": str(user["_id"]), "generation": generation}, routing_key=user["SynchronizationHostRestriction"] if "SynchronizationHostRestriction" in user and user["SynchronizationHostRestriction"] else "")
32 | print("Scheduled %d users at %s" % (len(scheduled_ids), datetime.utcnow()))
33 |
34 | time.sleep(1)
35 |
--------------------------------------------------------------------------------
/sync_watchdog.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db, close_connections
2 | from tapiriik.sync import SyncStep
3 | import os
4 | import signal
5 | import socket
6 | from datetime import timedelta, datetime
7 |
8 | print("Sync watchdog run at %s" % datetime.now())
9 |
10 | host = socket.gethostname()
11 |
12 | for worker in db.sync_workers.find({"Host": host}):
13 | # Does the process still exist?
14 | alive = True
15 | try:
16 | os.kill(worker["Process"], 0)
17 | except os.error:
18 | print("%s is no longer alive" % worker)
19 | alive = False
20 |
21 | # Has it been stalled for too long?
22 | if worker["State"] == SyncStep.List:
23 | timeout = timedelta(minutes=45) # This can take a loooooooong time
24 | else:
25 | timeout = timedelta(minutes=10) # But everything else shouldn't
26 |
27 | if alive and worker["Heartbeat"] < datetime.utcnow() - timeout:
28 | print("%s timed out" % worker)
29 | os.kill(worker["Process"], signal.SIGKILL)
30 | alive = False
31 |
32 | # Clear it from the database if it's not alive.
33 | if not alive:
34 | db.sync_workers.remove({"_id": worker["_id"]})
35 | # Unlock users attached to it.
36 | db.users.update({"SynchronizationWorker": worker["Process"], "SynchronizationHost": host}, {"$unset":{"SynchronizationWorker": True}}, multi=True)
37 |
38 | db.sync_watchdogs.update({"Host": host}, {"Host": host, "Timestamp": datetime.utcnow()}, upsert=True)
39 |
40 | close_connections()
41 |
--------------------------------------------------------------------------------
/sync_worker.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import os
3 | # I'm trying to track down where some missing seconds are going in the sync process
4 | # Will grep these out of the log at some later date
5 | def worker_message(state):
6 | print("Sync worker %d %s at %s" % (os.getpid(), state, datetime.now()))
7 |
8 | worker_message("booting")
9 |
10 | from tapiriik.requests_lib import patch_requests_with_default_timeout, patch_requests_source_address
11 | from tapiriik import settings
12 | from tapiriik.database import db, close_connections
13 | from pymongo import ReturnDocument
14 | import sys
15 | import subprocess
16 | import socket
17 |
18 | RecycleInterval = 2 # Time spent rebooting workers < time spent wrangling Python memory management.
19 |
20 | oldCwd = os.getcwd()
21 | WorkerVersion = subprocess.Popen(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, cwd=os.path.dirname(__file__)).communicate()[0].strip()
22 | os.chdir(oldCwd)
23 |
24 | def sync_heartbeat(state, user=None):
25 | db.sync_workers.update({"_id": heartbeat_rec_id}, {"$set": {"Heartbeat": datetime.utcnow(), "State": state, "User": user}})
26 |
27 | worker_message("initialized")
28 |
29 | # Moved this flush before the sync_workers upsert for a rather convoluted reason:
30 | # Some of the sync servers were encountering filesystem corruption, causing the FS to be remounted as read-only.
31 | # Then, when a sync worker would start, it would insert a record in sync_workers then immediately die upon calling flush - since output is piped to a log file on the read-only FS.
32 | # Supervisor would dutifully restart the worker again and again, causing sync_workers to quickly fill up.
33 | # ...which is a problem, since it doesn't have indexes on Process or Host - what later lookups were based on. So, the database would be brought to a near standstill.
34 | # Theoretically, the watchdog would clean up these records soon enough - but since it too logs to a file, it would crash removing only a few stranded records
35 | # By flushing the logs before we insert, it should crash before filling that collection up.
36 | # (plus, we no longer query with Process/Host in sync_hearbeat)
37 |
38 | sys.stdout.flush()
39 | heartbeat_rec = db.sync_workers.find_one_and_update(
40 | {
41 | "Process": os.getpid(),
42 | "Host": socket.gethostname()
43 | }, {
44 | "$set": {
45 | "Process": os.getpid(),
46 | "Host": socket.gethostname(),
47 | "Heartbeat": datetime.utcnow(),
48 | "Startup": datetime.utcnow(),
49 | "Version": WorkerVersion,
50 | "Index": settings.WORKER_INDEX,
51 | "State": "startup"
52 | }
53 | }, upsert=True,
54 | return_document=ReturnDocument.AFTER)
55 | heartbeat_rec_id = heartbeat_rec["_id"]
56 |
57 | patch_requests_with_default_timeout(timeout=60)
58 |
59 | if isinstance(settings.HTTP_SOURCE_ADDR, list):
60 | settings.HTTP_SOURCE_ADDR = settings.HTTP_SOURCE_ADDR[settings.WORKER_INDEX % len(settings.HTTP_SOURCE_ADDR)]
61 | patch_requests_source_address((settings.HTTP_SOURCE_ADDR, 0))
62 |
63 | print(" %d -> Index %s\n -> Interface %s" % (os.getpid(), settings.WORKER_INDEX, settings.HTTP_SOURCE_ADDR))
64 |
65 | # We defer including the main body of the application till here so the settings aren't captured before we've set them up.
66 | # The better way would be to defer initializing services until they're requested, but it's 10:30 and this will work just as well.
67 | from tapiriik.sync import Sync
68 |
69 | Sync.InitializeWorkerBindings()
70 |
71 | sync_heartbeat("ready")
72 |
73 | worker_message("ready")
74 |
75 | Sync.PerformGlobalSync(heartbeat_callback=sync_heartbeat, version=WorkerVersion, max_users=RecycleInterval)
76 |
77 | worker_message("shutting down cleanly")
78 | db.sync_workers.remove({"_id": heartbeat_rec_id})
79 | close_connections()
80 | worker_message("shut down")
81 | sys.stdout.flush()
82 |
--------------------------------------------------------------------------------
/tapiriik/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/__init__.py
--------------------------------------------------------------------------------
/tapiriik/auth/credential_storage.py:
--------------------------------------------------------------------------------
1 | from Crypto.Cipher import PKCS1_OAEP
2 | from Crypto.PublicKey import RSA
3 | from Crypto.Hash import SHA
4 | from Crypto import Random
5 | from tapiriik.settings import CREDENTIAL_STORAGE_PUBLIC_KEY, CREDENTIAL_STORAGE_PRIVATE_KEY
6 | import copy
7 |
8 | #### note about tapiriik and credential storage ####
9 | # Some services require a username and password for every action - so they need to be stored in recoverable form
10 | # (namely: Garmin Connect's current "API")
11 | # I've done my best to mitigate the risk that these credentials ever be compromised, but the risk can never be eliminated
12 | # If you're not comfortable with it, you can opt to not have your credentials stored, instead entering them on every sync
13 |
14 | class CredentialStore:
15 | def Init():
16 | _key = RSA.importKey(CREDENTIAL_STORAGE_PRIVATE_KEY if CREDENTIAL_STORAGE_PRIVATE_KEY else CREDENTIAL_STORAGE_PUBLIC_KEY)
17 | CredentialStore._cipher = PKCS1_OAEP.new(_key)
18 |
19 | def Encrypt(cred):
20 | data = CredentialStore._cipher.encrypt(cred.encode("UTF-8"))
21 |
22 | # We store the plaintext credential so that the web server can use it later during the same request.
23 | # After the request terminates, it'll be GC'd more-or-less as quickly as the incoming request data itself.
24 | return ShadowedCredential(data, cred)
25 |
26 | def Decrypt(data):
27 | # Check if we encrypted the data in the same session, and use that instead (we might not have the private key).
28 | if isinstance(data, ShadowedCredential):
29 | return data.plaintext
30 |
31 | # I kind of doubt anyone could get away with a timing attack on the sycnhronization workers.
32 | # But, dear comment-reader, I'm sure you're now contemplating the possibilities...
33 | # So PKCS#1 OAEP it is.
34 | return CredentialStore._cipher.decrypt(data).decode("UTF-8")
35 |
36 | def FlattenShadowedCredentials(auth_dict):
37 | flat_auth_dict = copy.deepcopy(auth_dict)
38 | for k, v in auth_dict.items():
39 | if isinstance(v, ShadowedCredential):
40 | flat_auth_dict[k] = v.ciphertext
41 | return flat_auth_dict
42 |
43 | class ShadowedCredential:
44 | def __init__(self, ciphertext, plaintext):
45 | self.ciphertext = ciphertext
46 | self.plaintext = plaintext
47 |
48 | CredentialStore.Init()
49 |
--------------------------------------------------------------------------------
/tapiriik/auth/totp.py:
--------------------------------------------------------------------------------
1 | import time
2 | import base64
3 | import math
4 | import hmac
5 | import hashlib
6 | import struct
7 |
8 |
9 | class TOTP:
10 | def Get(secret):
11 | counter = struct.pack(">Q", int(time.time() / 30))
12 | key = base64.b32decode(secret.upper().encode())
13 | csp = hmac.new(key, counter, hashlib.sha1)
14 | res = csp.digest()
15 | offset = res[19] & 0xf
16 | code_pre = struct.unpack(">I", res[offset:offset + 4])[0]
17 | code_pre = code_pre & 0x7fffffff
18 | return int(code_pre % (math.pow(10, 6)))
19 |
--------------------------------------------------------------------------------
/tapiriik/database/__init__.py:
--------------------------------------------------------------------------------
1 | from pymongo import MongoClient, MongoReplicaSetClient
2 | from tapiriik.settings import MONGO_HOST, MONGO_REPLICA_SET, MONGO_CLIENT_OPTIONS, REDIS_HOST, REDIS_CLIENT_OPTIONS
3 |
4 | # MongoDB
5 |
6 | client_class = MongoClient if not MONGO_REPLICA_SET else MongoReplicaSetClient
7 | if MONGO_REPLICA_SET:
8 | MONGO_CLIENT_OPTIONS["replicaSet"] = MONGO_REPLICA_SET
9 |
10 | _connection = client_class(host=MONGO_HOST, **MONGO_CLIENT_OPTIONS)
11 |
12 | db = _connection["tapiriik"]
13 | cachedb = _connection["tapiriik_cache"]
14 | tzdb = _connection["tapiriik_tz"]
15 | # The main db currently has an unfortunate lock contention rate
16 | ratelimit = _connection["tapiriik_ratelimit"]
17 |
18 | # Redis
19 | if REDIS_HOST:
20 | import redis as redis_client
21 | redis = redis_client.Redis(host=REDIS_HOST, **REDIS_CLIENT_OPTIONS)
22 | else:
23 | redis = None # Must be defined
24 |
25 | def close_connections():
26 | try:
27 | _connection.close()
28 | except:
29 | pass
30 |
31 | import atexit
32 | atexit.register(close_connections)
33 |
--------------------------------------------------------------------------------
/tapiriik/database/tz.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import tzdb
2 | from bson.son import SON
3 |
4 | def TZLookup(lat, lng):
5 | pt = [lng, lat]
6 | res = tzdb.boundaries.find_one({"Boundary": {"$geoIntersects": {"$geometry": {"type":"Point", "coordinates": pt}}}}, {"TZID": True})
7 | if not res:
8 | res = tzdb.boundaries.find_one({"Boundary": {"$near": {"$geometry": {"type": "Point", "coordinates": pt}, "$maxDistance": 200000}}}, {"TZID": True})
9 | res = res["TZID"] if res else None
10 | if not res or res == "uninhabited":
11 | res = round(lng / 15)
12 | return res
13 |
--------------------------------------------------------------------------------
/tapiriik/frontend/app.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import AerobiaConfig from "./components/aerobiaConfig";
5 | import LocalExporterConfig from "./components/localExporter/localExporterConfig";
6 | import EmptyComponent from "./components/empty";
7 |
8 | var componentToRender;
9 |
10 | switch(window.props.component.toLowerCase()) {
11 | case "aerobia":
12 | componentToRender = AerobiaConfig;
13 | break;
14 | case "localexporter":
15 | componentToRender = LocalExporterConfig;
16 | break;
17 | default:
18 | componentToRender = EmptyComponent;
19 | }
20 |
21 | ReactDOM.render(
22 | React.createElement(componentToRender, window.props),
23 | window.react_mount,
24 | )
--------------------------------------------------------------------------------
/tapiriik/frontend/components/aerobiaConfig.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import CSRFToken from './csrftoken'
4 | import RuleList from './ruleList'
5 | import ExportConfig from './exportConfig'
6 |
7 | const urlGears = (userid, token) =>
8 | `http://old15.aerobia.ru/users/${userid}/equipments?authentication_token=${token}`
9 |
10 | class AerobiaConfig extends React.Component {
11 | constructor(props) {
12 | super(props)
13 | this.state = {
14 | gears: [],
15 | requestFailed: false,
16 | gearRules: props.config.gearRules,
17 | export: props.config.export
18 | }
19 | }
20 |
21 | // componentDidMount() {
22 | // fetch("https://cors-anywhere.herokuapp.com/" + urlGears(this.props.aerobiaId, this.props.userToken))
23 | // .then(response => {
24 | // if (!response.ok) {
25 | // throw Error("Network request failed");
26 | // }
27 | // return response.text();
28 | // })
29 | // .then(d => {
30 | // var parser = new DOMParser();
31 | // var page = parser.parseFromString(d, "text/html");
32 | // var gears = [];
33 | // var itemNodes = page.getElementsByClassName("item");
34 | // if (!itemNodes.length) {
35 | // return gears;
36 | // }
37 | // itemNodes = Array.prototype.slice.call(itemNodes);
38 | // itemNodes.forEach(n => {
39 | // var itemData = n.getElementsByTagName("p")[0];
40 | // itemData = itemData.getElementsByTagName("a")[0];
41 | // var gearUrl = itemData.getAttribute("href").split("/");
42 | // gears.push({
43 | // id: gearUrl[gearUrl.length - 1],
44 | // name: itemData.innerText
45 | // });
46 | // });
47 | // return gears;
48 | // })
49 | // .then(d => {
50 | // this.setState({
51 | // gears: d
52 | // })
53 | // }, () => {
54 | // this.setState({
55 | // requestFailed: true
56 | // })
57 | // })
58 | // }
59 |
60 | updateExportSettings(newState) {
61 | this.setState({export: newState});
62 | }
63 |
64 | updateGearRules(newState) {
65 | this.setState({gearRules: newState.rules});
66 | }
67 |
68 | render() {
69 | const { sportTypes } = this.props;
70 | // if (this.state.gears.length == 0)
71 | // return (
72 | //
73 | //
Aerobia advanced settings
74 | //
Loading gear...
75 | //
76 | // );
77 |
78 | return (
79 |
80 |
Aerobia advanced settings
81 |
82 |
83 | this.updateExportSettings(newState)}
86 | />
87 |
88 |
89 | {/*
90 |
Default gear rules:
91 |
this.updateGearRules(newState)}
96 | />
97 | */}
98 |
105 |
106 | );
107 | }
108 | }
109 |
110 | export default AerobiaConfig;
111 |
--------------------------------------------------------------------------------
/tapiriik/frontend/components/csrftoken.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function getCookie(name) {
4 | var cookieValue = null;
5 | if (document.cookie && document.cookie !== '') {
6 | var cookies = document.cookie.split(';');
7 | for (var i = 0; i < cookies.length; i++) {
8 | var cookie = jQuery.trim(cookies[i]);
9 | if (cookie.substring(0, name.length + 1) === (name + '=')) {
10 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
11 | break;
12 | }
13 | }
14 | }
15 | return cookieValue;
16 | }
17 |
18 | var csrftoken = getCookie('csrftoken');
19 |
20 | const CSRFToken = () => {
21 | return (
22 |
23 | );
24 | };
25 | export default CSRFToken;
--------------------------------------------------------------------------------
/tapiriik/frontend/components/empty.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default class EmptyComponent extends React.Component {
4 | render() {
5 | return (
6 | Component missing!
7 | );
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tapiriik/frontend/components/exportConfig.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default class ExportConfig extends React.Component {
4 | constructor(props) {
5 | super(props)
6 | this.state = {
7 | upload_media_content: props.data.upload_media_content,
8 | min_report_length: props.data.min_report_length
9 | }
10 | }
11 |
12 | toggleChecked() {
13 | this.setState({ upload_media_content: !this.state.upload_media_content }, () => {
14 | this.props.handleChange(this.state);
15 | });
16 | }
17 |
18 | lengthChanged(e) {
19 | this.setState({ min_report_length: parseInt(e.target.value) }, () => {
20 | this.props.handleChange(this.state);
21 | });
22 | }
23 |
24 | render() {
25 | return (
26 |
45 | )
46 | }
47 | }
--------------------------------------------------------------------------------
/tapiriik/frontend/components/localExporter/localExporterConfig.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CSRFToken from "../csrftoken";
3 |
4 | export default class LocalExporterConfig extends React.Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {
8 | download_only_media_content: props.config.download_only_media_content
9 | }
10 | }
11 |
12 | toggleChecked() {
13 | this.setState({ download_only_media_content: !this.state.download_only_media_content });
14 | }
15 |
16 | submitForm() {
17 | $.post('/localExporterConfig', {'config': JSON.stringify(this.state)});
18 | window.location.href = "/";
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
Local Exporter settings
25 |
26 |
27 | Download only posts and photos
28 |
33 |
34 |
35 |
42 |
43 | )
44 | }
45 | }
--------------------------------------------------------------------------------
/tapiriik/frontend/components/ruleLine.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Select from 'react-select'
3 | import 'react-select/dist/react-select.css'
4 |
5 | export default class RuleLine extends React.Component {
6 | constructor(props) {
7 | super(props)
8 | this.state = {
9 | id: props.data.id,
10 | selectedSport: props.data.sport,
11 | selectedGear: props.data.gear,
12 | }
13 | }
14 |
15 | handleSportChange = (selectedOption) => {
16 | var sport = '';
17 | if (selectedOption) {
18 | sport = selectedOption.value;
19 | }
20 | this.setState({ selectedSport: sport }, () => {
21 | this.props.handleChange(this.state);
22 | });
23 | console.log(`Selected: ${sport}`);
24 | }
25 |
26 | handleGearChange = (selectedOption) => {
27 | var gear = [];
28 | if (selectedOption) {
29 | gear = selectedOption.map((o) => o.value);
30 | }
31 | this.setState({ selectedGear: gear }, () => {
32 | this.props.handleChange(this.state);
33 | });
34 | console.log(`Selected: ${gear}`);
35 | }
36 |
37 | handleDelete = (event) => {
38 | event.preventDefault();
39 | this.props.handleDelete();
40 | }
41 |
42 | render() {
43 | const { selectedSport, selectedGear } = this.state;
44 | const { sportTypes, gears } = this.props
45 | const sports = sportTypes.map(function (e) {
46 | var obj = {};
47 | obj['value'] = e;
48 | obj['label'] = e;
49 | return obj;
50 | });
51 | const gearData = gears.map(function (e) {
52 | var obj = {};
53 | obj['value'] = e.id;
54 | obj['label'] = e.name;
55 | return obj;
56 | });
57 | return (
58 |
59 |
60 | this.handleSportChange(obj)}
65 | options={sports}
66 | />
67 |
68 |
69 | this.handleGearChange(obj)}
75 | options={gearData}
76 | />
77 |
78 |
79 |
80 | );
81 | }
82 | }
--------------------------------------------------------------------------------
/tapiriik/frontend/components/ruleList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RuleLine from './ruleLine'
3 |
4 | export default class RuleList extends React.Component {
5 | constructor(props) {
6 | super(props)
7 | this.state = {
8 | rules: props.data
9 | }
10 | }
11 |
12 | handleNewRule = (event) => {
13 | var timestamp = (new Date()).getTime();
14 | this.state.rules.push({
15 | id: timestamp
16 | });
17 | this.setState({ rules: this.state.rules });
18 | console.log('Rule added');
19 | event.preventDefault();
20 | }
21 |
22 | deleteRule(id) {
23 | this.setState(prevState => ({
24 | rules: prevState.rules.filter(e => e.id != id)
25 | }), () => {
26 | this.props.handleChange(this.state);
27 | });
28 | console.log('Rule deleted');
29 | }
30 |
31 | updateRule(newState) {
32 | let rules = [...this.state.rules];
33 | let index = rules.findIndex(el => el.id === newState.id);
34 | rules[index] = {...rules[index], gear: newState.selectedGear};
35 | rules[index] = {...rules[index], sport: newState.selectedSport};
36 | this.setState({ rules }, () => {
37 | this.props.handleChange(this.state);
38 | });
39 | }
40 |
41 | render() {
42 | const { sportTypes, gears } = this.props
43 | var ruleLines = this.state.rules.map(function (rule) {
44 | return (
45 | this.deleteRule(rule.id)}
51 | handleChange={(newState) => this.updateRule(newState)} />
52 | );
53 | }.bind(this));
54 | return (
55 |
56 | {ruleLines}
57 |
58 |
59 | );
60 | }
61 | }
--------------------------------------------------------------------------------
/tapiriik/local_settings.py.example:
--------------------------------------------------------------------------------
1 | # Look in settings.py for more settings to override
2 | # including mongodb, rabbitmq, and redis connection settings
3 |
4 | # This is the url that is used for redirects after logging in to each service
5 | # It only needs to be accessible to the client browser
6 | WEB_ROOT = "http://localhost:8000"
7 |
8 | # This is where sync logs show up
9 | # It is the only directory that needs to be writable by the webapp user
10 | USER_SYNC_LOGS = "./"
11 |
12 | # Folder to store user data
13 | USER_DATA_FILES = "./user_export_data"
14 |
15 | # These settings are used to communicate with each respective service
16 | # Register your installation with each service to get these values
17 |
18 | # http://beginnertriathlete.com/discussion/contact.asp?department=api
19 | BT_APIKEY = "####"
20 |
21 | DROPBOX_FULL_APP_KEY = "####"
22 | DROPBOX_FULL_APP_SECRET = "####"
23 |
24 | DROPBOX_APP_KEY = "####"
25 | DROPBOX_APP_SECRET = "####"
26 |
27 | ENDOMONDO_CLIENT_KEY = "####"
28 | ENDOMONDO_CLIENT_SECRET = "####"
29 |
30 | MOTIVATO_PREMIUM_USERS_LIST_URL = "http://..."
31 |
32 | NIKEPLUS_CLIENT_NAME = "####"
33 | NIKEPLUS_CLIENT_ID = "####"
34 | NIKEPLUS_CLIENT_SECRET = "####"
35 |
36 | PULSSTORY_CLIENT_ID="####"
37 | PULSSTORY_CLIENT_SECRET="####"
38 |
39 | RUNKEEPER_CLIENT_ID="####"
40 | RUNKEEPER_CLIENT_SECRET="####"
41 |
42 | RWGPS_APIKEY = "####"
43 |
44 | SETIO_CLIENT_ID = "####"
45 | SETIO_CLIENT_SECRET = "####"
46 |
47 | SINGLETRACKER_CLIENT_ID = "####"
48 | SINGLETRACKER_CLIENT_SECRET = "####"
49 |
50 | # See http://api.smashrun.com for info.
51 | # For now, you need to email hi@smashrun.com for access
52 | SMASHRUN_CLIENT_ID = "####"
53 | SMASHRUN_CLIENT_SECRET = "####"
54 |
55 | SPORTTRACKS_CLIENT_ID = "####"
56 | SPORTTRACKS_CLIENT_SECRET = "####"
57 |
58 | STRAVA_CLIENT_SECRET = "####"
59 | STRAVA_CLIENT_ID = "####"
60 | STRAVA_RATE_LIMITS = []
61 |
62 | POLAR_CLIENT_SECRET = "####"
63 | POLAR_CLIENT_ID = "####"
64 | POLAR_RATE_LIMITS = []
65 |
66 | TRAINASONE_SERVER_URL = "https://beta.trainasone.com"
67 | TRAINASONE_CLIENT_SECRET = "####"
68 | TRAINASONE_CLIENT_ID = "####"
69 |
70 | TRAININGPEAKS_CLIENT_ID = "####"
71 | TRAININGPEAKS_CLIENT_SECRET = "####"
72 | TRAININGPEAKS_CLIENT_SCOPE = "cats:cuddle dogs:throw-frisbee"
73 | TRAININGPEAKS_API_BASE_URL = "https://api.trainingpeaks.com"
74 | TRAININGPEAKS_OAUTH_BASE_URL = "https://oauth.trainingpeaks.com"
75 |
76 | DECATHLONCOACH_CLIENT_SECRET = "####"
77 | DECATHLONCOACH_CLIENT_ID = "####"
78 | DECATHLONCOACH_API_KEY = "####"
79 |
--------------------------------------------------------------------------------
/tapiriik/locale/fr/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/locale/fr/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/tapiriik/locale/ru/LC_MESSAGES/django.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/locale/ru/LC_MESSAGES/django.mo
--------------------------------------------------------------------------------
/tapiriik/messagequeue/__init__.py:
--------------------------------------------------------------------------------
1 | from kombu import Connection
2 | from tapiriik.settings import RABBITMQ_BROKER_URL
3 | mq = Connection(RABBITMQ_BROKER_URL, transport_options={'confirm_publish': True})
4 | mq.connect()
5 |
--------------------------------------------------------------------------------
/tapiriik/payments/__init__.py:
--------------------------------------------------------------------------------
1 | from .payments import Payments
2 | from .external.provider_base import ExternalPaymentProvider
3 |
--------------------------------------------------------------------------------
/tapiriik/payments/external/__init__.py:
--------------------------------------------------------------------------------
1 | from .provider_base import *
2 | from .motivato import *
3 |
--------------------------------------------------------------------------------
/tapiriik/payments/external/motivato.py:
--------------------------------------------------------------------------------
1 | from .provider_base import ExternalPaymentProvider
2 | from tapiriik.database import db
3 | from tapiriik.settings import MOTIVATO_PREMIUM_USERS_LIST_URL
4 | import requests
5 |
6 | class MotivatoExternalPaymentProvider(ExternalPaymentProvider):
7 | ID = "motivato"
8 |
9 | def RefreshPaymentStateForExternalIDs(self, external_ids):
10 | from tapiriik.services import Service, ServiceRecord
11 | external_ids = [str(x) for x in external_ids]
12 | connections = [ServiceRecord(x) for x in db.connections.find({"Service": "motivato", "ExternalID": {"$in": external_ids}})]
13 | users = db.users.find({"ConnectedServices.ID": {"$in": [x._id for x in connections]}})
14 | for user in users:
15 | my_connection = [x for x in connections if x._id in [y["ID"] for y in user["ConnectedServices"]]][0]
16 | # Defer to the actual service module, where all the session stuff is set up
17 | state = Service.FromID("motivato")._getPaymentState(my_connection)
18 | self.ApplyPaymentState(user, state, my_connection.ExternalID, duration=None)
19 |
20 | def RefreshPaymentState(self):
21 | from tapiriik.services import ServiceRecord
22 | from tapiriik.payments import Payments
23 | from tapiriik.auth import User
24 |
25 | external_ids = requests.get(MOTIVATO_PREMIUM_USERS_LIST_URL).json()
26 | connections = [ServiceRecord(x) for x in db.connections.find({"Service": "motivato", "ExternalID": {"$in": external_ids}})]
27 | users = list(db.users.find({"ConnectedServices.ID": {"$in": [x._id for x in connections]}}))
28 | payments = []
29 |
30 | # Pull relevant payment objects and associate with users
31 | for user in users:
32 | my_connection = [x for x in connections if x._id in [y["ID"] for y in user["ConnectedServices"]]][0]
33 | pmt = Payments.EnsureExternalPayment(self.ID, my_connection.ExternalID, duration=None)
34 | payments.append(pmt)
35 | User.AssociateExternalPayment(user, pmt, skip_deassoc=True)
36 |
37 | # Bulk-remove these payments from users who don't own them (more or less - it'll leave anyone who switched remote accounts)
38 | db.users.update({"_id": {"$nin": [x["_id"] for x in users]}}, {"$pull": {"ExternalPayments": {"_id": {"$in": [x["_id"] for x in payments]}}}}, multi=True)
39 |
40 | # We don't bother unsetting users who are no longer on the list - they'll be refreshed at their next sync
41 |
42 |
43 | ExternalPaymentProvider.Register(MotivatoExternalPaymentProvider())
--------------------------------------------------------------------------------
/tapiriik/payments/external/provider_base.py:
--------------------------------------------------------------------------------
1 |
2 | class ExternalPaymentProvider:
3 | _providers = []
4 | def FromID(id):
5 | return [x for x in ExternalPaymentProvider._providers if x.ID == id][0]
6 |
7 | def Register(instance):
8 | ExternalPaymentProvider._providers.append(instance)
9 |
10 | ID = None
11 | def RefreshPaymentStateForExternalIDs(self, external_ids):
12 | raise NotImplemented
13 |
14 | def RefreshPaymentState(self):
15 | raise NotImplemented
16 |
17 | def ApplyPaymentState(self, user, state, externalID, duration=None):
18 | from tapiriik.payments import Payments
19 | from tapiriik.auth import User
20 | if state:
21 | pmt = Payments.EnsureExternalPayment(self.ID, externalID, duration)
22 | User.AssociateExternalPayment(user, pmt)
23 | else:
24 | Payments.ExpireExternalPayment(self.ID, externalID)
25 |
--------------------------------------------------------------------------------
/tapiriik/requests_lib.py:
--------------------------------------------------------------------------------
1 | # For whatever reason there's no built-in way to specify a global timeout for requests operations.
2 | # socket.setdefaulttimeout doesn't work since requests overriddes the default with its own default.
3 | # There's probably a better way to do this in requests 2.x, but...
4 |
5 | def patch_requests_with_default_timeout(timeout):
6 | import requests
7 | old_request = requests.Session.request
8 | def new_request(*args, **kwargs):
9 | if "timeout" not in kwargs:
10 | kwargs["timeout"] = timeout
11 | return old_request(*args, **kwargs)
12 | requests.Session.request = new_request
13 |
14 | def patch_requests_no_verify_ssl():
15 | import requests
16 | old_request = requests.Session.request
17 | def new_request(*args, **kwargs):
18 | kwargs.update({"verify": False})
19 | return old_request(*args, **kwargs)
20 | requests.Session.request = new_request
21 |
22 | # Not really patching requests here, but...
23 | def patch_requests_source_address(new_source_address):
24 | import socket
25 | old_create_connection = socket.create_connection
26 | def new_create_connection(address, timeout=None, source_address=None):
27 | if address[1] in [80, 443]:
28 | return old_create_connection(address, timeout, new_source_address)
29 | else:
30 | return old_create_connection(address, timeout, source_address)
31 | socket.create_connection = new_create_connection
32 |
33 | def patch_requests_user_agent(user_agent):
34 | import requests
35 | old_request = requests.Session.request
36 | def new_request(*args, **kwargs):
37 | headers = kwargs.get("headers", {})
38 | headers = headers if headers else {}
39 | headers["User-Agent"] = user_agent
40 | kwargs["headers"] = headers
41 | return old_request(*args, **kwargs)
42 | requests.Session.request = new_request
43 |
--------------------------------------------------------------------------------
/tapiriik/services/Aerobia/__init__.py:
--------------------------------------------------------------------------------
1 | from .aerobia import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/BeginnerTriathlete/__init__.py:
--------------------------------------------------------------------------------
1 | from .beginnertriathlete import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/DecathlonCoach/__init__.py:
--------------------------------------------------------------------------------
1 | from .decathloncoach import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/Dropbox/__init__.py:
--------------------------------------------------------------------------------
1 | from .dropbox import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/Endomondo/__init__.py:
--------------------------------------------------------------------------------
1 | from .endomondo import *
--------------------------------------------------------------------------------
/tapiriik/services/GarminConnect/__init__.py:
--------------------------------------------------------------------------------
1 | from .garminconnect import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/LocalExporter/__init__.py:
--------------------------------------------------------------------------------
1 | from .localExporter import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/Motivato/__init__.py:
--------------------------------------------------------------------------------
1 | from .motivato import *
--------------------------------------------------------------------------------
/tapiriik/services/NikePlus/__init__.py:
--------------------------------------------------------------------------------
1 | from .nikeplus import *
--------------------------------------------------------------------------------
/tapiriik/services/PolarFlow/__init__.py:
--------------------------------------------------------------------------------
1 | from .polarflow import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/PolarPersonalTrainer/__init__.py:
--------------------------------------------------------------------------------
1 | from .polarpersonaltrainer import *
2 | from .pptToTcx import convert
--------------------------------------------------------------------------------
/tapiriik/services/Pulsstory/__init__.py:
--------------------------------------------------------------------------------
1 | from .pulsstory import PulsstoryService
--------------------------------------------------------------------------------
/tapiriik/services/RideWithGPS/__init__.py:
--------------------------------------------------------------------------------
1 | from .rwgps import *
--------------------------------------------------------------------------------
/tapiriik/services/RunKeeper/__init__.py:
--------------------------------------------------------------------------------
1 | from .runkeeper import *
--------------------------------------------------------------------------------
/tapiriik/services/Setio/__init__.py:
--------------------------------------------------------------------------------
1 | from .setio import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/Singletracker/__init__.py:
--------------------------------------------------------------------------------
1 | from .singletracker import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/Smashrun/__init__.py:
--------------------------------------------------------------------------------
1 | from .smashrun import SmashrunService
2 |
--------------------------------------------------------------------------------
/tapiriik/services/SportTracks/__init__.py:
--------------------------------------------------------------------------------
1 | from .sporttracks import *
--------------------------------------------------------------------------------
/tapiriik/services/Strava/__init__.py:
--------------------------------------------------------------------------------
1 | from .strava import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/TrainAsONE/__init__.py:
--------------------------------------------------------------------------------
1 | from .trainasone import *
2 |
--------------------------------------------------------------------------------
/tapiriik/services/TrainerRoad/__init__.py:
--------------------------------------------------------------------------------
1 | from .trainerroad import *
--------------------------------------------------------------------------------
/tapiriik/services/TrainingPeaks/__init__.py:
--------------------------------------------------------------------------------
1 | from .trainingpeaks import *
--------------------------------------------------------------------------------
/tapiriik/services/VeloHero/__init__.py:
--------------------------------------------------------------------------------
1 | from .velohero import *
--------------------------------------------------------------------------------
/tapiriik/services/__init__.py:
--------------------------------------------------------------------------------
1 | from .service_base import *
2 | from .api import *
3 | from tapiriik.services.RunKeeper import RunKeeperService
4 | RunKeeper = RunKeeperService()
5 | from tapiriik.services.Strava import StravaService
6 | Strava = StravaService()
7 | from tapiriik.services.Endomondo import EndomondoService
8 | Endomondo = EndomondoService()
9 | from tapiriik.services.Dropbox import DropboxService
10 | Dropbox = DropboxService()
11 | from tapiriik.services.GarminConnect import GarminConnectService
12 | GarminConnect = GarminConnectService()
13 | from tapiriik.services.SportTracks import SportTracksService
14 | SportTracks = SportTracksService()
15 | from tapiriik.services.RideWithGPS import RideWithGPSService
16 | RideWithGPS = RideWithGPSService()
17 | from tapiriik.services.TrainAsONE import TrainAsONEService
18 | TrainAsONE = TrainAsONEService()
19 | from tapiriik.services.TrainingPeaks import TrainingPeaksService
20 | TrainingPeaks = TrainingPeaksService()
21 | from tapiriik.services.Motivato import MotivatoService
22 | Motivato = MotivatoService()
23 | from tapiriik.services.NikePlus import NikePlusService
24 | NikePlus = NikePlusService()
25 | from tapiriik.services.VeloHero import VeloHeroService
26 | VeloHero = VeloHeroService()
27 | from tapiriik.services.TrainerRoad import TrainerRoadService
28 | TrainerRoad = TrainerRoadService()
29 | from tapiriik.services.Smashrun import SmashrunService
30 | Smashrun = SmashrunService()
31 | from tapiriik.services.BeginnerTriathlete import BeginnerTriathleteService
32 | BeginnerTriathlete = BeginnerTriathleteService()
33 | from tapiriik.services.Pulsstory import PulsstoryService
34 | Pulsstory = PulsstoryService()
35 | from tapiriik.services.Setio import SetioService
36 | Setio = SetioService()
37 | from tapiriik.services.Singletracker import SingletrackerService
38 | Singletracker = SingletrackerService()
39 | from tapiriik.services.Aerobia import AerobiaService
40 | Aerobia = AerobiaService()
41 | from tapiriik.services.PolarFlow import PolarFlowService
42 | PolarFlow = PolarFlowService()
43 | from tapiriik.services.DecathlonCoach import DecathlonCoachService
44 | DecathlonCoach = DecathlonCoachService()
45 | from tapiriik.services.PolarPersonalTrainer import PolarPersonalTrainerService
46 | PolarPersonalTrainer = PolarPersonalTrainerService()
47 | from tapiriik.services.LocalExporter import LocalExporterService
48 | LocalExporter = LocalExporterService()
49 |
50 | PRIVATE_SERVICES = []
51 | try:
52 | from private.tapiriik.services import *
53 | except ImportError:
54 | pass
55 |
56 | from .service import *
57 | from .service_record import *
58 |
--------------------------------------------------------------------------------
/tapiriik/services/api.py:
--------------------------------------------------------------------------------
1 | class ServiceExceptionScope:
2 | Account = "account"
3 | Service = "service"
4 | # Unlike Account and Service-level blocking exceptions, these are implemented via ActivityRecord.FailureCounts
5 | # Eventually, all errors might be stored in ActivityRecords
6 | Activity = "activity"
7 |
8 | class ServiceException(Exception):
9 | def __init__(self, message, scope=ServiceExceptionScope.Service, block=False, user_exception=None, trigger_exhaustive=True):
10 | Exception.__init__(self, message)
11 | self.Message = message
12 | self.UserException = user_exception
13 | self.Block = block
14 | self.Scope = scope
15 | self.TriggerExhaustive = trigger_exhaustive
16 |
17 | def __str__(self):
18 | return self.Message + " (user " + str(self.UserException) + " )"
19 |
20 | class ServiceWarning(ServiceException):
21 | pass
22 |
23 | class APIException(ServiceException):
24 | pass
25 |
26 | class APIWarning(ServiceWarning):
27 | pass
28 |
29 | # Theoretically, APIExcludeActivity should actually be a ServiceException with block=True, scope=Activity
30 | # It's on the to-do list.
31 |
32 | class APIExcludeActivity(Exception):
33 | def __init__(self, message, activity=None, activity_id=None, permanent=True, user_exception=None):
34 | Exception.__init__(self, message)
35 | self.Message = message
36 | self.Activity = activity
37 | self.ExternalActivityID = activity_id
38 | self.Permanent = permanent
39 | self.UserException = user_exception
40 |
41 | def __str__(self):
42 | return self.Message + " (activity " + str(self.ExternalActivityID) + ")"
43 |
44 | class UserExceptionType:
45 | # Account-level exceptions (not a hardcoded thing, just to keep these seperate)
46 | Authorization = "auth"
47 | RenewPassword = "renew_password"
48 | Locked = "locked"
49 | AccountFull = "full"
50 | AccountExpired = "expired"
51 | AccountUnpaid = "unpaid" # vs. expired, which implies it was at some point function, via payment or trial or otherwise.
52 | NonAthleteAccount = "non_athlete_account" # trainingpeaks
53 | NotAValidEmail = "not_a_valid_email" # localExporter
54 | EmailsDoNotMatch = "emails_do_not_match" # localExporter
55 | GCUploadConsent = "gc_upload_consent" # EU User must grant upload consent on GC
56 |
57 | # Activity-level exceptions
58 | FlowException = "flow"
59 | Private = "private"
60 | NoSupplier = "nosupplier"
61 | NotTriggered = "notrigger"
62 | Deferred = "deferred" # They've instructed us not to synchronize activities for some time after they complete
63 | PredatesWindow = "predates_window" # They've instructed us not to synchronize activities before some date
64 | RateLimited = "ratelimited"
65 | MissingCredentials = "credentials_missing" # They forgot to check the "Remember these details" box
66 | NotConfigured = "config_missing" # Don't think this error is even possible any more.
67 | StationaryUnsupported = "stationary"
68 | NonGPSUnsupported = "nongps"
69 | TypeUnsupported = "type_unsupported"
70 | InsufficientData = "data_insufficient" # Some services demand more data than others provide (ahem, N+)
71 | DownloadError = "download"
72 | ListingError = "list" # Cases when a service fails listing, so nothing can be uploaded to it.
73 | UploadError = "upload"
74 | SanityError = "sanity"
75 | Corrupt = "corrupt" # Kind of a scary term for what's generally "some data is missing"
76 | Untagged = "untagged"
77 | LiveTracking = "live"
78 | UnknownTZ = "tz_unknown"
79 | System = "system"
80 | Other = "other"
81 |
82 | class UserException:
83 | def __init__(self, type, extra=None, intervention_required=False, clear_group=None):
84 | self.Type = type
85 | self.Extra = extra # Unimplemented - displayed as part of the error message.
86 | self.InterventionRequired = intervention_required # Does the user need to dismiss this error?
87 | self.ClearGroup = clear_group if clear_group else type # Used to group error messages displayed to the user, and let them clear a group that share a common cause.
88 |
--------------------------------------------------------------------------------
/tapiriik/services/exception_tools.py:
--------------------------------------------------------------------------------
1 | def strip_context(exc):
2 | exc.__context__ = exc.__cause__ = exc.__traceback__ = None
3 | return exc
--------------------------------------------------------------------------------
/tapiriik/services/ratelimiting.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import ratelimit as rl_db
2 | from pymongo.read_preferences import ReadPreference
3 | from datetime import datetime, timedelta
4 | import math
5 |
6 | class RateLimitExceededException(Exception):
7 | pass
8 |
9 | class RateLimit:
10 | def Limit(key):
11 | current_limits = rl_db.limits.find({"Key": key}, {"Max": 1, "Count": 1})
12 | for limit in current_limits:
13 | if limit["Max"] < limit["Count"]:
14 | # We can't continue without exceeding this limit
15 | # Don't want to halt the synchronization worker to wait for 15min-1 hour
16 | # So...
17 | raise RateLimitExceededException()
18 | rl_db.limits.update({"Key": key}, {"$inc": {"Count": 1}}, multi=True)
19 |
20 | def Refresh(key, limits):
21 | # Limits is in format [(timespan, max-count),...]
22 | # The windows are anchored at midnight
23 | # The timespan is used to uniquely identify limit instances between runs
24 | midnight = datetime.combine(datetime.utcnow().date(), datetime.min.time())
25 | time_since_midnight = (datetime.utcnow() - midnight)
26 |
27 | rl_db.limits.remove({"Key": key, "Expires": {"$lt": datetime.utcnow()}})
28 | current_limits = list(rl_db.limits.with_options(read_preference=ReadPreference.PRIMARY).find({"Key": key}, {"Duration": 1}))
29 | missing_limits = [x for x in limits if x[0].total_seconds() not in [limit["Duration"] for limit in current_limits]]
30 | for limit in missing_limits:
31 | window_start = midnight + timedelta(seconds=math.floor(time_since_midnight.total_seconds()/limit[0].total_seconds()) * limit[0].total_seconds())
32 | window_end = window_start + limit[0]
33 | rl_db.limits.insert({"Key": key, "Count": 0, "Duration": limit[0].total_seconds(), "Max": limit[1], "Expires": window_end})
34 |
--------------------------------------------------------------------------------
/tapiriik/services/rollback.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db
2 | from tapiriik.auth import User
3 | from tapiriik.services import Service
4 | import datetime
5 | import logging
6 | import json
7 | from bson.objectid import ObjectId
8 | logger = logging.getLogger(__name__)
9 |
10 | class RollbackTask:
11 | def __new__(cls, dbRec):
12 | if not dbRec:
13 | return None
14 | return super(RollbackTask, cls).__new__(cls)
15 |
16 | def __init__(self, dbRec):
17 | self.__dict__.update(dbRec)
18 |
19 | def _create(user):
20 | # Pull all the records that need to be rolled back
21 | logger.info("Finding activities for %s" % user["_id"])
22 | conns = User.GetConnectionRecordsByUser(user)
23 | my_services = [conn.Service.ID for conn in conns]
24 | my_ext_ids = [conn.ExternalID for conn in conns]
25 | logger.info("Scanning uploads table for %s accounts with %s extids" % (my_services, my_ext_ids))
26 | uploads = db.uploaded_activities.find({"Service": {"$in": my_services}, "UserExternalID": {"$in": my_ext_ids}})
27 | pending_deletions = {}
28 | for upload in uploads:
29 | svc = upload["Service"]
30 | upload_id = upload["ExternalID"]
31 | svc_ext_id = upload["UserExternalID"]
32 | # Filter back down to the pairing we actually need
33 | if my_services.index(svc) != my_ext_ids.index(svc_ext_id):
34 | continue
35 | if svc not in pending_deletions:
36 | pending_deletions[svc] = []
37 | pending_deletions[svc].append(upload_id)
38 |
39 | # Another case of "I should have an ORM"
40 | return RollbackTask({"PendingDeletions": pending_deletions})
41 |
42 | def Create(user):
43 | task = RollbackTask._create(user)
44 | uid = db.rollback_tasks.insert({"PendingDeletions": task.PendingDeletions, "Created": datetime.datetime.utcnow(), "UserID": user["_id"]})
45 | logger.info("Created rollback task %s" % uid)
46 | task._id = uid
47 | return task
48 |
49 | def Get(id):
50 | dbRec = db.rollback_tasks.find_one({"_id": ObjectId(id)})
51 | if not dbRec:
52 | return
53 | return RollbackTask(dbRec)
54 |
55 | def json(self):
56 | # Augment with the requisite URLs
57 | self.ActivityURLs = {svc: {} for svc in self.PendingDeletions.keys()}
58 | for svc_id, urls in self.ActivityURLs.items():
59 | svc = Service.FromID(svc_id)
60 | for upload in self.PendingDeletions[svc_id]:
61 | try:
62 | urls[upload] = svc.UserUploadedActivityURL(upload)
63 | except NotImplementedError:
64 | pass
65 | self.PendingDeletionCount = sum([len(v) for k, v in self.PendingDeletions.items()])
66 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date) else str(obj)
67 | return json.dumps(self.__dict__, default=dthandler)
68 |
69 | def Schedule(self):
70 | db.rollback_tasks.update({"_id": self._id}, {"$set": {"Scheduled": datetime.datetime.utcnow()}})
71 | from rollback_worker import schedule_rollback_task
72 | schedule_rollback_task(str(self._id))
73 |
74 | def Execute(self):
75 | logger.info("Starting rollback %s" % self._id)
76 | deletion_status = {}
77 | user = User.Get(self.UserID)
78 | for svc_id, upload_ids in self.PendingDeletions.items():
79 | svcrec = User.GetConnectionRecord(user, svc_id)
80 | deletion_status[svc_id] = {}
81 | if not svcrec.Service.SupportsActivityDeletion:
82 | continue
83 | for upload_id in upload_ids:
84 | logger.info("Deleting activity %s on %s" % (upload_id, svc_id))
85 | try:
86 | svcrec.Service.DeleteActivity(svcrec, upload_id)
87 | except Exception as e:
88 | deletion_status[svc_id][str(upload_id)] = False
89 | logger.exception("Deletion failed - %s" % e)
90 | else:
91 | deletion_status[svc_id][str(upload_id)] = True
92 | db.rollback_tasks.update({"_id": self._id}, {"$set": {"DeletionStatus": deletion_status}})
93 | logger.info("Finished rollback %s" % self._id)
94 |
--------------------------------------------------------------------------------
/tapiriik/services/service_record.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import cachedb, db
2 | import copy
3 |
4 | class ServiceRecord:
5 | def __new__(cls, dbRec):
6 | if not dbRec:
7 | return None
8 | return super(ServiceRecord, cls).__new__(cls)
9 | def __init__(self, dbRec):
10 | self.__dict__.update(dbRec)
11 | def __repr__(self):
12 | return " " + str(self.__dict__)
13 |
14 | def __eq__(self, other):
15 | return self._id == other._id
16 |
17 | def __ne__(self, other):
18 | return not self.__eq__(other)
19 |
20 | def __deepcopy__(self, x):
21 | return ServiceRecord(self.__dict__)
22 |
23 | ExcludedActivities = {}
24 | Config = {}
25 | PartialSyncTriggerSubscribed = False
26 |
27 | @property
28 | def Service(self):
29 | from tapiriik.services import Service
30 | return Service.FromID(self.__dict__["Service"])
31 |
32 | def HasExtendedAuthorizationDetails(self, persisted_only=False):
33 | if not self.Service.RequiresExtendedAuthorizationDetails:
34 | return False
35 | if "ExtendedAuthorization" in self.__dict__ and self.ExtendedAuthorization:
36 | return True
37 | if persisted_only:
38 | return False
39 | return cachedb.extendedAuthDetails.find({"ID": self._id}).limit(1).count()
40 |
41 | def SetPartialSyncTriggerSubscriptionState(self, subscribed):
42 | db.connections.update({"_id": self._id}, {"$set": {"PartialSyncTriggerSubscribed": subscribed}})
43 |
44 | def GetConfiguration(self):
45 | from tapiriik.services import Service
46 | svc = self.Service
47 | config = copy.deepcopy(Service._globalConfigurationDefaults)
48 | config.update(svc.ConfigurationDefaults)
49 | config.update(self.Config)
50 | return config
51 |
52 | def SetConfiguration(self, config, no_save=False, drop_existing=False):
53 | from tapiriik.services import Service
54 | sparseConfig = {}
55 | if not drop_existing:
56 | sparseConfig = copy.deepcopy(self.GetConfiguration())
57 | sparseConfig.update(config)
58 |
59 | svc = self.Service
60 | svc.ConfigurationUpdating(self, config, self.GetConfiguration())
61 | keys_to_delete = []
62 | for k, v in sparseConfig.items():
63 | if (k in svc.ConfigurationDefaults and svc.ConfigurationDefaults[k] == v) or (k in Service._globalConfigurationDefaults and Service._globalConfigurationDefaults[k] == v):
64 | keys_to_delete.append(k) # it's the default, we can not store it
65 | for k in keys_to_delete:
66 | del sparseConfig[k]
67 | self.Config = sparseConfig
68 | if not no_save:
69 | db.connections.update({"_id": self._id}, {"$set": {"Config": sparseConfig}})
70 |
--------------------------------------------------------------------------------
/tapiriik/services/sessioncache.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from tapiriik.database import redis
3 | import pickle
4 |
5 | class SessionCache:
6 | def __init__(self, scope, lifetime, freshen_on_get=False):
7 | self._lifetime = lifetime
8 | self._autorefresh = freshen_on_get
9 | self._scope = scope
10 | self._cacheKey = "sessioncache:%s:%s" % (self._scope, "%s")
11 |
12 | def Get(self, pk, freshen=False):
13 | res = redis.get(self._cacheKey % pk)
14 | if res:
15 | try:
16 | res = pickle.loads(res)
17 | except pickle.UnpicklingError:
18 | self.Delete(pk)
19 | res = None
20 | else:
21 | if self._autorefresh or freshen:
22 | redis.expire(self._cacheKey % pk, self._lifetime)
23 | return res
24 |
25 | def Set(self, pk, value, lifetime=None):
26 | lifetime = lifetime or self._lifetime
27 | redis.setex(self._cacheKey % pk, pickle.dumps(value), lifetime)
28 |
29 | def Delete(self, pk):
30 | redis.delete(self._cacheKey % pk)
31 |
--------------------------------------------------------------------------------
/tapiriik/services/statistic_calculator.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from .interchange import WaypointType
3 |
4 | class ActivityStatisticCalculator:
5 | ImplicitPauseTime = timedelta(minutes=1, seconds=5)
6 |
7 | def CalculateDistance(act, startWpt=None, endWpt=None):
8 | import math
9 | dist = 0
10 | altHold = None # seperate from the lastLoc variable, since we want to hold the altitude as long as required
11 | lastTimestamp = lastLoc = None
12 |
13 | flatWaypoints = act.GetFlatWaypoints()
14 |
15 | if not startWpt:
16 | startWpt = flatWaypoints[0]
17 | if not endWpt:
18 | endWpt = flatWaypoints[-1]
19 |
20 | for x in range(flatWaypoints.index(startWpt), flatWaypoints.index(endWpt) + 1):
21 | timeDelta = flatWaypoints[x].Timestamp - lastTimestamp if lastTimestamp else None
22 | lastTimestamp = flatWaypoints[x].Timestamp
23 |
24 | if flatWaypoints[x].Type == WaypointType.Pause or (timeDelta and timeDelta > ActivityStatisticCalculator.ImplicitPauseTime):
25 | lastLoc = None # don't count distance while paused
26 | continue
27 |
28 | loc = flatWaypoints[x].Location
29 | if loc is None or loc.Longitude is None or loc.Latitude is None:
30 | # Used to throw an exception in this case, but the TCX schema allows for location-free waypoints, so we'll just patch over it.
31 | continue
32 |
33 | if loc and lastLoc:
34 | altHold = lastLoc.Altitude if lastLoc.Altitude is not None else altHold
35 | latRads = loc.Latitude * math.pi / 180
36 | meters_lat_degree = 1000 * 111.13292 + 1.175 * math.cos(4 * latRads) - 559.82 * math.cos(2 * latRads)
37 | meters_lon_degree = 1000 * 111.41284 * math.cos(latRads) - 93.5 * math.cos(3 * latRads)
38 | dx = (loc.Longitude - lastLoc.Longitude) * meters_lon_degree
39 | dy = (loc.Latitude - lastLoc.Latitude) * meters_lat_degree
40 | if loc.Altitude is not None and altHold is not None: # incorporate the altitude when possible
41 | dz = loc.Altitude - altHold
42 | else:
43 | dz = 0
44 | dist += math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
45 | lastLoc = loc
46 |
47 | return dist
48 |
49 | def CalculateTimerTime(act, startWpt=None, endWpt=None):
50 | flatWaypoints = []
51 | for lap in act.Laps:
52 | flatWaypoints.append(lap.Waypoints)
53 |
54 | if len(flatWaypoints) < 3:
55 | # Either no waypoints, or one at the start and one at the end
56 | raise ValueError("Not enough waypoints to calculate timer time")
57 | duration = timedelta(0)
58 | if not startWpt:
59 | startWpt = flatWaypoints[0]
60 | if not endWpt:
61 | endWpt = flatWaypoints[-1]
62 | lastTimestamp = None
63 | for x in range(flatWaypoints.index(startWpt), flatWaypoints.index(endWpt) + 1):
64 | wpt = flatWaypoints[x]
65 | delta = wpt.Timestamp - lastTimestamp if lastTimestamp else None
66 | lastTimestamp = wpt.Timestamp
67 | if wpt.Type is WaypointType.Pause:
68 | lastTimestamp = None
69 | elif delta and delta > act.ImplicitPauseTime:
70 | delta = None # Implicit pauses
71 | if delta:
72 | duration += delta
73 | if duration.total_seconds() == 0 and startWpt is None and endWpt is None:
74 | raise ValueError("Zero-duration activity")
75 | return duration
76 |
77 | def CalculateAverageMaxHR(act, startWpt=None, endWpt=None):
78 | flatWaypoints = act.GetFlatWaypoints()
79 |
80 | # Python can handle 600+ digit numbers, think it can handle this
81 | maxHR = 0
82 | cumulHR = 0
83 | samples = 0
84 |
85 | if not startWpt:
86 | startWpt = flatWaypoints[0]
87 | if not endWpt:
88 | endWpt = flatWaypoints[-1]
89 |
90 | for x in range(flatWaypoints.index(startWpt), flatWaypoints.index(endWpt) + 1):
91 | wpt = flatWaypoints[x]
92 | if wpt.HR:
93 | if wpt.HR > maxHR:
94 | maxHR = wpt.HR
95 | cumulHR += wpt.HR
96 | samples += 1
97 |
98 | if not samples:
99 | return None, None
100 |
101 | cumulHR = cumulHR / samples
102 | return cumulHR, maxHR
103 |
104 |
105 |
--------------------------------------------------------------------------------
/tapiriik/services/stream_sampling.py:
--------------------------------------------------------------------------------
1 | class StreamSampler:
2 | def SampleWithCallback(callback, streams):
3 | """
4 | *streams should be a dict in format {"stream1":[(ts1,val1), (ts2, val2)...]...} where ts is a numerical offset from the activity start.
5 | Expect callback(time_offset, stream1=value1, stream2=value2) in chronological order. Stream values may be None
6 | All samples are represented - none are dropped
7 | """
8 |
9 | # Collate the individual streams into discrete waypoints.
10 | # There is no global sampling rate - waypoints are created for every new datapoint in any stream (simultaneous datapoints are included in the same waypoint)
11 | # Resampling is based on the last known value of the stream - no interpolation or nearest-neighbour.
12 |
13 | streamData = streams
14 | streams = list(streams.keys())
15 | print("Handling streams %s" % streams)
16 |
17 | stream_indices = dict([(stream, -1) for stream in streams]) # -1 meaning the stream has yet to start
18 | stream_lengths = dict([(stream, len(streamData[stream])) for stream in streams])
19 |
20 | currentTimeOffset = 0
21 |
22 | while True:
23 | advance_stream = None
24 | advance_offset = None
25 | for stream in streams:
26 | if stream_indices[stream] + 1 == stream_lengths[stream]:
27 | continue # We're at the end - can't advance
28 | if advance_offset is None or streamData[stream][stream_indices[stream] + 1][0] - currentTimeOffset < advance_offset:
29 | advance_offset = streamData[stream][stream_indices[stream] + 1][0] - currentTimeOffset
30 | advance_stream = stream
31 | if not advance_stream:
32 | break # We've hit the end of every stream, stop
33 | # Update the current time offset based on the key advancing stream (others may still be behind)
34 | currentTimeOffset = streamData[advance_stream][stream_indices[advance_stream] + 1][0]
35 | # Advance streams with the current timestamp, including advance_stream
36 | for stream in streams:
37 | if stream_indices[stream] + 1 == stream_lengths[stream]:
38 | continue # We're at the end - can't advance
39 | if streamData[stream][stream_indices[stream] + 1][0] == currentTimeOffset: # Don't need to consider <, as then that stream would be advance_stream
40 | stream_indices[stream] += 1
41 | callbackDataArgs = {}
42 | for stream in streams:
43 | if stream_indices[stream] >= 0:
44 | callbackDataArgs[stream] = streamData[stream][stream_indices[stream]][1]
45 | callback(currentTimeOffset, **callbackDataArgs)
46 |
--------------------------------------------------------------------------------
/tapiriik/sync/__init__.py:
--------------------------------------------------------------------------------
1 | from .sync import *
--------------------------------------------------------------------------------
/tapiriik/testing/__init__.py:
--------------------------------------------------------------------------------
1 | from .sync import *
2 | from .interchange import *
3 | from .gpx import *
4 | from .statistics import *
5 | from .tcx import *
6 |
--------------------------------------------------------------------------------
/tapiriik/testing/gpx.py:
--------------------------------------------------------------------------------
1 | from tapiriik.testing.testtools import TestTools, TapiriikTestCase
2 | from tapiriik.services.gpx import GPXIO
3 |
4 |
5 | class GPXTests(TapiriikTestCase):
6 | def test_constant_representation(self):
7 | ''' ensures that gpx import/export is symetric '''
8 |
9 | svcA, other = TestTools.create_mock_services()
10 | svcA.SupportsHR = svcA.SupportsCadence = svcA.SupportsTemp = True
11 | svcA.SupportsPower = svcA.SupportsCalories = False
12 | act = TestTools.create_random_activity(svcA, tz=True, withPauses=False)
13 |
14 | mid = GPXIO.Dump(act)
15 |
16 | act2 = GPXIO.Parse(bytes(mid, "UTF-8"))
17 | act2.TZ = act.TZ # we need to fake this since local TZ isn't defined in GPX files, and TZ discovery will flail with random activities
18 | act2.AdjustTZ()
19 | act.Stats.Distance = act2.Stats.Distance = None # same here
20 |
21 | self.assertActivitiesEqual(act2, act)
22 |
--------------------------------------------------------------------------------
/tapiriik/testing/interchange.py:
--------------------------------------------------------------------------------
1 | from tapiriik.testing.testtools import TestTools, TapiriikTestCase
2 |
3 | from tapiriik.services import Service
4 | from tapiriik.services.interchange import Activity, ActivityType
5 |
6 | from datetime import datetime, timedelta
7 |
8 |
9 | class InterchangeTests(TapiriikTestCase):
10 |
11 | def test_round_precise_time(self):
12 | ''' Some services might return really exact times, while others would round to the second - needs to be accounted for in hash algo '''
13 | actA = Activity()
14 | actA.StartTime = datetime(1, 2, 3, 4, 5, 6, 7)
15 | actB = Activity()
16 | actB.StartTime = datetime(1, 2, 3, 4, 5, 6, 7) + timedelta(0, 0.1337)
17 |
18 | actA.CalculateUID()
19 | actB.CalculateUID()
20 |
21 | self.assertEqual(actA.UID, actB.UID)
22 |
23 | def test_constant_representation_rk(self):
24 | ''' ensures that all services' API clients are consistent through a simulated download->upload cycle '''
25 | # runkeeper
26 | rkSvc = Service.FromID("runkeeper")
27 | act = TestTools.create_random_activity(rkSvc, rkSvc.SupportedActivities[0], withLaps=False)
28 | record = rkSvc._createUploadData(act)
29 | record["has_path"] = act.GPS # RK helpfully adds a "has_path" entry if we have waypoints.
30 | returnedAct = rkSvc._populateActivity(record)
31 | act.Name = None # RK doesn't have a "name" field, so it's fudged into the notes, but not really
32 | rkSvc._populateActivityWaypoints(record, returnedAct)
33 | # RK deliberately doesn't set timezone..
34 | returnedAct.EnsureTZ()
35 | self.assertActivitiesEqual(returnedAct, act)
36 |
37 | # can't test Strava well this way, the upload and download formats are entirely different
38 |
39 | # can't test endomondo - upload data all constructed in upload function.. needs refactor?
40 |
41 |
42 | def test_activity_specificity_resolution(self):
43 | # Mountain biking is more specific than just cycling
44 | self.assertEqual(ActivityType.PickMostSpecific([ActivityType.Cycling, ActivityType.MountainBiking]), ActivityType.MountainBiking)
45 |
46 | # But not once we mix in an unrelated activity - pick the first
47 | self.assertEqual(ActivityType.PickMostSpecific([ActivityType.Cycling, ActivityType.MountainBiking, ActivityType.Swimming]), ActivityType.Cycling)
48 |
49 | # Duplicates
50 | self.assertEqual(ActivityType.PickMostSpecific([ActivityType.Cycling, ActivityType.MountainBiking, ActivityType.MountainBiking]), ActivityType.MountainBiking)
51 |
52 | # One
53 | self.assertEqual(ActivityType.PickMostSpecific([ActivityType.MountainBiking]), ActivityType.MountainBiking)
54 |
55 | # With None
56 | self.assertEqual(ActivityType.PickMostSpecific([None, ActivityType.MountainBiking]), ActivityType.MountainBiking)
57 |
58 | # All None
59 | self.assertEqual(ActivityType.PickMostSpecific([None, None]), ActivityType.Other)
60 |
61 | # Never pick 'Other' given a better option
62 | self.assertEqual(ActivityType.PickMostSpecific([ActivityType.Other, ActivityType.MountainBiking]), ActivityType.MountainBiking)
63 |
64 | # Normal w/ Other + None
65 | self.assertEqual(ActivityType.PickMostSpecific([ActivityType.Other, ActivityType.Cycling, None, ActivityType.MountainBiking]), ActivityType.MountainBiking)
66 |
--------------------------------------------------------------------------------
/tapiriik/testing/tcx.py:
--------------------------------------------------------------------------------
1 | from tapiriik.testing.testtools import TestTools, TapiriikTestCase
2 | from tapiriik.services.tcx import TCXIO
3 |
4 | import os
5 |
6 | class TCXTests(TapiriikTestCase):
7 | def test_constant_representation(self):
8 | ''' ensures that tcx import/export is symetric '''
9 | script_dir = os.path.dirname(__file__)
10 | rel_path = "data/test1.tcx"
11 | source_file_path = os.path.join(script_dir, rel_path)
12 | with open(source_file_path, 'r') as testfile:
13 | data = testfile.read()
14 |
15 | act = TCXIO.Parse(data.encode('utf-8'))
16 | act.PrerenderedFormats.clear()
17 | new_data = TCXIO.Dump(act)
18 | act2 = TCXIO.Parse(new_data.encode('utf-8'))
19 | rel_path = "data/output1.tcx"
20 | new_file_path = os.path.join(script_dir, rel_path)
21 | with open(new_file_path, "w") as new_file:
22 | new_file.write(new_data)
23 |
24 | self.assertActivitiesEqual(act2, act)
25 |
26 | def test_garmin_tcx_export(self):
27 | ''' ensures that tcx exported from Garmin Connect can be correctly parsed '''
28 | script_dir = os.path.dirname(__file__)
29 | rel_path = "data/garmin_parse_1.tcx"
30 | source_file_path = os.path.join(script_dir, rel_path)
31 | with open(source_file_path, 'r') as testfile:
32 | data = testfile.read()
33 |
34 | act = TCXIO.Parse(data.encode('utf-8'))
35 | act.PrerenderedFormats.clear()
36 | new_data = TCXIO.Dump(act)
37 | act2 = TCXIO.Parse(new_data.encode('utf-8'))
38 | rel_path = "data/output2.tcx"
39 | new_file_path = os.path.join(script_dir, rel_path)
40 | with open(new_file_path, "w") as new_file:
41 | new_file.write(new_data)
42 |
43 | self.assertActivitiesEqual(act2, act)
--------------------------------------------------------------------------------
/tapiriik/web/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/__init__.py
--------------------------------------------------------------------------------
/tapiriik/web/context_processors.py:
--------------------------------------------------------------------------------
1 | from tapiriik.services import Service
2 | from tapiriik.auth import User
3 | from tapiriik.sync import Sync
4 | from tapiriik.settings import SITE_VER, PP_WEBSCR, PP_BUTTON_ID, SOFT_LAUNCH_SERVICES, DISABLED_SERVICES, WITHDRAWN_SERVICES, CELEBRATION_MODES
5 | from tapiriik.database import db
6 | from datetime import datetime
7 | import json
8 |
9 |
10 | def providers(req):
11 | return {"service_providers": Service.List()}
12 |
13 | def config(req):
14 | in_diagnostics = "diagnostics" in req.path
15 | return {"config": {"minimumSyncInterval": Sync.MinimumSyncInterval.seconds, "siteVer": SITE_VER, "pp": {"url": PP_WEBSCR, "buttonId": PP_BUTTON_ID}, "soft_launch": SOFT_LAUNCH_SERVICES, "disabled_services": DISABLED_SERVICES, "withdrawn_services": WITHDRAWN_SERVICES, "in_diagnostics": in_diagnostics}, "hidden_infotips": req.COOKIES.get("infotip_hide", None)}
16 |
17 | def user(req):
18 | return {"user":req.user}
19 |
20 | def js_bridge(req):
21 | serviceInfo = {}
22 |
23 | for svc in Service.List():
24 | if svc.ID in WITHDRAWN_SERVICES:
25 | continue
26 | if req.user is not None:
27 | svcRec = User.GetConnectionRecord(req.user, svc.ID) # maybe make the auth handler do this only once?
28 | else:
29 | svcRec = None
30 | info = {
31 | "DisplayName": svc.DisplayName,
32 | "DisplayAbbreviation": svc.DisplayAbbreviation,
33 | "AuthenticationType": svc.AuthenticationType,
34 | "UsesExtendedAuth": svc.RequiresExtendedAuthorizationDetails,
35 | "AuthorizationURL": svc.UserAuthorizationURL,
36 | "NoFrame": svc.AuthenticationNoFrame,
37 | "ReceivesActivities": svc.ReceivesActivities,
38 | "Configurable": svc.Configurable,
39 | "RequiresConfiguration": False # by default
40 | }
41 | if svcRec:
42 | if svc.Configurable:
43 | if svc.ID == "dropbox": # dirty hack alert, but better than dumping the auth details in their entirety
44 | info["AccessLevel"] = "full" if svcRec.Authorization["Full"] else "normal"
45 | info["RequiresConfiguration"] = svc.RequiresConfiguration(svcRec)
46 | info["Config"] = svcRec.GetConfiguration()
47 | info["HasExtendedAuth"] = svcRec.HasExtendedAuthorizationDetails()
48 | info["PersistedExtendedAuth"] = svcRec.HasExtendedAuthorizationDetails(persisted_only=True)
49 | info["ExternalID"] = svcRec.ExternalID
50 | info["BlockFlowTo"] = []
51 | info["Connected"] = svcRec is not None
52 | serviceInfo[svc.ID] = info
53 | if req.user is not None:
54 | flowExc = User.GetFlowExceptions(req.user)
55 | for exc in flowExc:
56 | if exc["Source"]["Service"] not in serviceInfo or exc["Target"]["Service"] not in serviceInfo:
57 | continue # Withdrawn services
58 | if "ExternalID" in serviceInfo[exc["Source"]["Service"]] and exc["Source"]["ExternalID"] != serviceInfo[exc["Source"]["Service"]]["ExternalID"]:
59 | continue # this is an old exception for a different connection
60 | if "ExternalID" in serviceInfo[exc["Target"]["Service"]] and exc["Target"]["ExternalID"] != serviceInfo[exc["Target"]["Service"]]["ExternalID"]:
61 | continue # same as above
62 | serviceInfo[exc["Source"]["Service"]]["BlockFlowTo"].append(exc["Target"]["Service"])
63 | return {"js_bridge_serviceinfo": json.dumps(serviceInfo)}
64 |
65 |
66 | def stats(req):
67 | return {"stats": db.stats.find_one()}
68 |
69 | def celebration_mode(req):
70 | active_config = None
71 | now = datetime.now()
72 | for date_range, config in CELEBRATION_MODES.items():
73 | if date_range[0] <= now and date_range[1] >= now:
74 | active_config = config
75 | break
76 | return {"celebration_mode": active_config}
77 |
--------------------------------------------------------------------------------
/tapiriik/web/email.py:
--------------------------------------------------------------------------------
1 | from django.template.loader import get_template
2 | from django.template import Context
3 | from django.core.mail import EmailMultiAlternatives
4 | from django.conf import settings
5 |
6 | def generate_message_from_template(template, context):
7 | context["STATIC_URL"] = settings.STATIC_URL
8 | # Mandrill is set up to inline the CSS and generate a plaintext copy.
9 | html_message = get_template(template).render(Context(context)).strip()
10 | context["plaintext"] = True
11 | plaintext_message = get_template(template).render(Context(context)).strip()
12 | return html_message, plaintext_message
13 |
14 | def send_email(recipient_list, subject, html_message, plaintext_message=None):
15 | if type(recipient_list) is not list:
16 | recipient_list = [recipient_list]
17 |
18 | email = EmailMultiAlternatives(subject=subject, body=plaintext_message, from_email="exercisync ", to=recipient_list, headers={"Reply-To": "help@exercisync.com"})
19 | email.attach_alternative(html_message, "text/html")
20 | email.send()
--------------------------------------------------------------------------------
/tapiriik/web/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/tapiriik/web/startup.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import MiddlewareNotUsed
2 | import tapiriik.settings
3 | import subprocess
4 |
5 |
6 | class ServiceWebStartup:
7 | def __init__(self):
8 | from tapiriik.services import Service
9 | Service.WebInit()
10 | raise MiddlewareNotUsed
11 |
12 |
13 | class Startup:
14 | def __init__(self):
15 | tapiriik.settings.SITE_VER = subprocess.Popen(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE).communicate()[0].strip()
16 | raise MiddlewareNotUsed
17 |
--------------------------------------------------------------------------------
/tapiriik/web/static/css/diagnostics.css:
--------------------------------------------------------------------------------
1 |
2 | .contentOuter {
3 | width:auto;
4 | height:auto;
5 | display:block;
6 | position: relative;
7 | padding:0;
8 | margin:0;
9 | }
10 |
11 | .contentWrap {
12 | display: block;
13 | }
14 |
15 | .content {
16 | }
17 |
18 | .logo {
19 | text-align: left;
20 | margin-left:30px;
21 | margin-top:-120px;
22 | width:auto;
23 | }
24 |
25 | .logo .sub {
26 | display: none;
27 | }
28 |
29 | .logoPad {
30 | margin-top:120px;
31 | }
32 |
33 | .contentOuterBorder {
34 | margin:20px;
35 | left:0;
36 | right:0;
37 | width:auto;
38 | }
39 |
40 | .footers {
41 | display: none;
42 | }
--------------------------------------------------------------------------------
/tapiriik/web/static/errors/upgrade.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | exercisync is offline
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
exercisync
16 |
is currently offline for upgrades (or something)
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
--------------------------------------------------------------------------------
/tapiriik/web/static/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/art/mapmyfitness-logo.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/art/mapmyfitness-logo.ai
--------------------------------------------------------------------------------
/tapiriik/web/static/img/connector-dot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/connector-dot.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/email/corner_bl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/email/corner_bl.gif
--------------------------------------------------------------------------------
/tapiriik/web/static/img/email/corner_br.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/email/corner_br.gif
--------------------------------------------------------------------------------
/tapiriik/web/static/img/email/corner_tl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/email/corner_tl.gif
--------------------------------------------------------------------------------
/tapiriik/web/static/img/email/corner_tr.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/email/corner_tr.gif
--------------------------------------------------------------------------------
/tapiriik/web/static/img/email/tapiriik-wordmark.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/email/tapiriik-wordmark.gif
--------------------------------------------------------------------------------
/tapiriik/web/static/img/email/trans.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/email/trans.gif
--------------------------------------------------------------------------------
/tapiriik/web/static/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/favicon-16x16.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/favicon-32x32.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/favicon.ico
--------------------------------------------------------------------------------
/tapiriik/web/static/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/favicon.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/garminconnect_useradd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/garminconnect_useradd.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/link.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/mstile-150x150.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/pp-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/pp-logo.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/privacy-cached.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/privacy-cached.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/privacy-no.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/privacy-no.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/privacy-optin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/privacy-optin.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/privacy-yes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/privacy-yes.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/rule-add.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/tapiriik/web/static/img/rule-delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tapiriik/web/static/img/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/aerobia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/aerobia.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/aerobia_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/aerobia_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/beginnertriathlete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/beginnertriathlete.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/beginnertriathlete_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/beginnertriathlete_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/decathloncoach.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/decathloncoach.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/decathloncoach_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/decathloncoach_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/dropbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/dropbox.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/dropbox_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/dropbox_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/endomondo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/endomondo.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/endomondo_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/endomondo_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/garminconnect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/garminconnect.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/garminconnect_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/garminconnect_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/localexporter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/localexporter.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/localexporter_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/localexporter_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/motivato.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/motivato.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/motivato_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/motivato_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/nikeplus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/nikeplus.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/nikeplus_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/nikeplus_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/polarflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/polarflow.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/polarflow_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/polarflow_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/polarpersonaltrainer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/polarpersonaltrainer.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/polarpersonaltrainer_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/polarpersonaltrainer_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/pulsstory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/pulsstory.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/pulsstory_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/pulsstory_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/runkeeper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/runkeeper.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/runkeeper_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/runkeeper_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/runsense.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/runsense.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/runsense_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/runsense_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/rwgps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/rwgps.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/rwgps_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/rwgps_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/setio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/setio.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/setio_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/setio_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/singletracker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/singletracker.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/singletracker_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/singletracker_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/smashrun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/smashrun.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/smashrun_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/smashrun_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/sporttracks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/sporttracks.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/sporttracks_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/sporttracks_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/strava.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/strava.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/strava_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/strava_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/trainasone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/trainasone.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/trainasone_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/trainasone_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/trainerroad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/trainerroad.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/trainerroad_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/trainerroad_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/trainingpeaks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/trainingpeaks.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/trainingpeaks_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/trainingpeaks_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/velohero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/velohero.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/services/velohero_l.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/services/velohero_l.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/snow.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-arrow.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-fail.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-go.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-go.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-info.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-ok.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-ok.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-pause.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-reauth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-reauth.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-settings.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/sync-spin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/sync-spin.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/tapiriik-arabic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/tapiriik-arabic.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/tapiriik-hebrew.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/tapiriik-hebrew.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/tapiriik-hindi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/tapiriik-hindi.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/tapiriik-inuktitut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/tapiriik-inuktitut.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/tapiriik-punjabi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/tapiriik-punjabi.png
--------------------------------------------------------------------------------
/tapiriik/web/static/img/trainingpeaks_download:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/trainingpeaks_download
--------------------------------------------------------------------------------
/tapiriik/web/static/img/unlink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/static/img/unlink.png
--------------------------------------------------------------------------------
/tapiriik/web/static/js/datepicker/pikaday-1.6.1.min.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";/*!
2 | * Pikaday
3 | * Copyright © 2014 David Bushell | BSD & MIT license | http://dbushell.com/
4 | */.pika-single{z-index:9999;display:block;position:relative;color:#333;background:#fff;border:1px solid #ccc;border-bottom-color:#bbb;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.pika-single:after,.pika-single:before{content:" ";display:table}.pika-single:after{clear:both}.pika-single.is-hidden{display:none}.pika-single.is-bound{position:absolute;box-shadow:0 5px 15px -5px rgba(0,0,0,.5)}.pika-lendar{float:left;width:240px;margin:8px}.pika-title{position:relative;text-align:center}.pika-label{display:inline-block;position:relative;z-index:9999;overflow:hidden;margin:0;padding:5px 3px;font-size:14px;line-height:20px;font-weight:700;background-color:#fff}.pika-title select{cursor:pointer;position:absolute;z-index:9998;margin:0;left:0;top:5px;opacity:0}.pika-next,.pika-prev{display:block;cursor:pointer;position:relative;outline:0;border:0;padding:0;width:20px;height:30px;text-indent:20px;white-space:nowrap;overflow:hidden;background-color:transparent;background-position:center center;background-repeat:no-repeat;background-size:75% 75%;opacity:.5}.pika-next:hover,.pika-prev:hover{opacity:1}.is-rtl .pika-next,.pika-prev{float:left;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg==)}.is-rtl .pika-prev,.pika-next{float:right;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII=)}.pika-next.is-disabled,.pika-prev.is-disabled{cursor:default;opacity:.2}.pika-select{display:inline-block}.pika-table{width:100%;border-collapse:collapse;border-spacing:0;border:0}.pika-table td,.pika-table th{width:14.285714285714286%;padding:0}.pika-table th{color:#999;font-size:12px;line-height:25px;font-weight:700;text-align:center}.pika-button{cursor:pointer;display:block;box-sizing:border-box;-moz-box-sizing:border-box;outline:0;border:0;margin:0;width:100%;padding:5px;color:#666;font-size:12px;line-height:15px;text-align:right;background:#f5f5f5}.pika-week{font-size:11px;color:#999}.is-today .pika-button{color:#3af;font-weight:700}.has-event .pika-button,.is-selected .pika-button{color:#fff;font-weight:700;background:#3af;box-shadow:inset 0 1px 3px #178fe5;border-radius:3px}.has-event .pika-button{background:#005da9;box-shadow:inset 0 1px 3px #0076c9}.is-disabled .pika-button,.is-inrange .pika-button{background:#d5e9f7}.is-startrange .pika-button{color:#fff;background:#6cb31d;box-shadow:none;border-radius:3px}.is-endrange .pika-button{color:#fff;background:#3af;box-shadow:none;border-radius:3px}.is-disabled .pika-button{pointer-events:none;cursor:default;color:#999;opacity:.3}.is-outside-current-month .pika-button{color:#999;opacity:.3}.is-selection-disabled{pointer-events:none;cursor:default}.pika-button:hover,.pika-row.pick-whole-week:hover .pika-button{color:#fff;background:#ff8000;box-shadow:none;border-radius:3px}.pika-table abbr{border-bottom:none;cursor:help}
5 | /*# sourceMappingURL=pikaday.min.css.map */
--------------------------------------------------------------------------------
/tapiriik/web/static/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/static/img/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/static/img/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/activities-dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% load services %}
4 | {% load users %}
5 | {% load displayutils %}
6 | {% block title %}activities{% endblock %}
7 |
8 | {% block content %}
9 | {% trans "Your activities" %}
10 | {% trans "Show only activities with sync errors:" %}
11 |
12 |
13 |
17 |
18 |
23 | {% trans "Present" %}
24 |
25 |
26 |
27 |
28 |
29 | {% trans "This activity was not synchronized to the following services:" %}
30 |
31 |
32 | {[ DisplayNameByService(absence.Service) ]} :
33 |
34 |
35 |
36 |
37 |
38 |
39 |
{% trans "exercisync doesn't know about any activities in your accounts" %} {% trans "Have you synchronized lately?" %}
40 |
{% trans "Loading..." %}
41 |
{% trans "If your account is currently synchronizing , those activities will appear here once the synchronization completes. All dates shown are in UTC." %}
42 |
43 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/auth/disconnect.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}disconnect{% endblock %}
4 | {% block content %}
5 | Disconnect {{ service.DisplayName }}?
6 |
11 |
12 | {% trans "(no data will be deleted - just no more synchronization will happen)" %}
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/auth/login.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}login{% endblock %}
4 | {% block content %}
5 | {% if res != None and res == false %}
6 | {% trans "There was a problem logging you into" %} {{ service.DisplayName }}
7 | {% endif %}
8 | {% trans "You're logging into" %} {{ serviceid }}
9 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/config/aerobia.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load services %}
3 | {% load users %}
4 | {% load displayutils %}
5 | {% load render_bundle from webpack_loader %}
6 |
7 | {% block title %}Configure Aerobia{% endblock %}
8 | {% block content %}
9 |
10 |
14 | {% render_bundle 'exercisyncApp' %}
15 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/config/dropbox.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% block title %}configure Dropbox{% endblock %}
3 | {% block content %}
4 | Dropbox configuration
5 |
10 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/config/localexporter.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load services %}
3 | {% load users %}
4 | {% load displayutils %}
5 | {% load render_bundle from webpack_loader %}
6 |
7 | {% block title %}Configure Local Exporter{% endblock %}
8 | {% block content %}
9 |
10 |
14 | {% render_bundle 'exercisyncApp' %}
15 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/error.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load displayutils %}
3 | {% block title %}error {{ error.value.exemplar }}{% endblock %}
4 | {% block content %}
5 |
6 |
7 |
{{ error.value.exemplar }} ({{ error|dict_get:'_id'|dict_get:'service' }})
8 |
Affected users:
9 | {% for user in affected_user_ids %}
10 |
{{ user }}
11 | {% endfor %}
12 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/error_error_not_found.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load displayutils %}
3 | {% block title %}diagnostics{% endblock %}
4 | {% block content %}
5 | Error not found
6 | (yay?)
7 |
8 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/error_user_not_found.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load displayutils %}
3 | {% block title %}diagnostics{% endblock %}
4 | {% block content %}
5 | User not found
6 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/errors.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load displayutils %}
3 | {% block title %}errors{% endblock %}
4 | {% block content %}
5 |
11 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/graphs.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load displayutils %}
3 | {% block title %}graphs{% endblock %}
4 | {% block content %}
5 |
6 |
7 |
8 |
42 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/login.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% block title %}diagnostics auth{% endblock %}
3 | {% block content %}
4 |
5 |
Super seecreet diagnostics dashboard login (ooooh! aaaaah!)
6 |
12 |
13 |
14 |
15 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/diag/payments.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load displayutils %}
3 | {% block title %}payments{% endblock %}
4 | {% block content %}
5 |
6 |
7 |
8 | Txn
9 | Timestamp
10 | Expiry
11 | Associated accounts
12 |
13 | {% for payment in payments %}
14 |
15 | {{ payment.Txn }}
16 | {{ payment.Timestamp }}
17 | {{ payment.Expiry }}
18 | {% for account in payment.Accounts %}{{ account }} {% endfor %}
19 |
20 | {% endfor %}
21 |
22 |
23 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/donation.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load displayutils %}
3 | {% trans "Support the project" %}
4 |
20 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/download.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% load services %}
4 | {% load users %}
5 | {% load displayutils %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/email/data_download.html:
--------------------------------------------------------------------------------
1 | {% extends "email/template.html" %}
2 | {% load i18n %}
3 | {% block content %}
4 | {% trans "Your data is ready!" %}
5 | {% trans "Please, download your data by following " %}{% trans "this link." %}
6 | {% endblock %}
7 | {% block plaintextcontent %}
8 | {% trans "Your data is ready!" %}
9 |
10 | {% trans "Please, download your data here: " %} {{ url|safe }}
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/email/payment_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends "email/template.html" %}
2 | {% load i18n %}
3 | {% block content %}
4 | {% trans "You're good to go" %}
5 | {% trans "Your payment has been processed and your tapiriik account is now set up for automatic synchronization. Of course, you can still visit " %}tapiriik.com {% trans "at any time to trigger an immediate synchronization." %}
6 | {% endblock %}
7 | {% block plaintextcontent %}
8 | {% trans "Your payment has been processed and your tapiriik account is now set up for automatic synchronization." %}
9 |
10 | {% trans "Of course, you can still visit {{ url|safe }} at any time to trigger an immediate synchronization." %}
11 |
12 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/email/payment_reclaim.html:
--------------------------------------------------------------------------------
1 | {% extends "email/template.html" %}
2 | {% load i18n %}
3 | {% block content %}
4 |
5 | {% trans "Your payment will be reclaimed & associated with the services you are currently connected to, and any you connect in the future." %}
6 | {% endblock %}
7 | {% block plaintextcontent %}
8 | {% trans "Use the following link to to reclaim your payment" %}:
9 |
10 | {{ url|safe }}
11 |
12 | {% trans "Your payment will be reclaimed & transferred to the services you are currently connected to, and any you connect in the future." %}
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/email/payment_renew.html:
--------------------------------------------------------------------------------
1 | {% extends "email/template.html" %}
2 | {% load i18n %}
3 | {% block content %}
4 | {% trans "It's been a good" %} {{ subscription_days }} {% trans "days" %}!
5 | {% trans "In the past" %} {{ subscription_fuzzy_time }} {% trans "or so, tapiriik has kept your" %} {{ account_list }} {% trans "in sync, automatically transferring" %} {% if distance %}{% trans "over" %} {{ distance }} {% trans "km of activities" %}{% else %}{% trans "your fitness activities night and day" %}{% endif %}.
6 |
7 | {% trans "Want to continue automatic synchronization?" %}
8 |
9 | {% trans "If you want to keep automatic synchronization for another year, visit " %}tapiriik.com , {% trans "connect to one of your accounts, and click the "Automatic Synchronization" link." %}
10 |
11 | {% trans "Don't worry - you won't automatically be billed." %}
12 | {% trans "(and if you were dissatisfied with your experience, please reply to this email to let me know why!)" %}
13 |
14 | {% endblock %}
15 | {% block plaintextcontent %}
16 | {% trans "It's been a good" %} {{ subscription_days }} {% trans "days" %}!
17 |
18 | {% trans "In the past" %} {{ subscription_fuzzy_time }} {% trans "or so, tapiriik has kept your" %} {{ account_list }} {% trans "in sync, automatically monitoring and transferring" %} {% if distance %}{% trans "over" %} {{ distance }} {% trans "km of your activities" %}{% else %}{% trans "your fitness activities night and day" %}{% endif %}.
19 |
20 | {% trans "If you want to keep automatic synchronization for another year, visit https://tapiriik.com then connect to one of your accounts, and click the "Automatic Synchronization" link." %}
21 |
22 | {% trans "Don't worry - you won't automatically be billed. If you were dissatisfied with your experience, please reply to this email to let me know why!)" %}
23 |
24 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/email/template.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if not plaintext %}
3 |
4 |
45 |
46 |
47 |
109 | {% else %}{% block plaintextcontent %}{% endblock %}
110 |
111 |
112 | Questions? Email me at help@exercisync.com{% endif %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/js-bridge.js:
--------------------------------------------------------------------------------
1 | {% load users %}
2 | {% load displayutils %}
3 | tapiriik.StaticURL = "{{ STATIC_URL }}";
4 | tapiriik.SiteVer = "{{ config.siteVer|slice:":7" }}";
5 | tapiriik.ServiceInfo = {{ js_bridge_serviceinfo|safe }}
6 | tapiriik.MinimumSyncInterval = {{ config.minimumSyncInterval }};
7 | {% if user %}tapiriik.User = {
8 | ConnectedServicesCount: {{ user.ConnectedServices|length }},
9 | ID: "{{ user|dict_get:'_id' }}",
10 | Timezone: "{{ user|dict_get:'Timezone' }}",
11 | Substitute: {{ user.Substitute|lower }},
12 | AutoSyncActive: {{ user|has_active_payment|lower }},
13 | Config: {{ user.Config|json|safe }}
14 | };
15 | {% endif %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/oauth-failure.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}login failed{% endblock %}
4 | {% block content %}
5 | {% trans "There was a problem logging you into" %} {{ service.DisplayName }}:
6 | {{ error }}
7 |
{% trans "Try logging in again - it might just be a temporary problem connecting to" %} {{ service.DisplayName }}. {% trans "If that doesn't help, try connecting this account using a different browser or device." %}
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/oauth-return.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/payment.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% load displayutils %}
3 |
26 | Automatic synchronization
27 |
71 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/payments/claim.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}payment claim{% endblock %}
4 | {% block content %}
5 | {% trans "Claim your payment" %}
6 |
7 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/payments/claim_return_fail.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}payment reclaim error{% endblock %}
4 | {% block content %}
5 | {% trans "Invalid payment claim code" %}
6 | {% trans "Please contact me if you're confused." %}
7 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/payments/confirmed.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}payment processed{% endblock %}
4 | {% block content %}
5 | {% trans "Thanks" %}!
6 | {% trans "Your exercisync account is now set up for automatic synchronization. You can " %}{% trans "head back to the dashboard" %} {% trans "or close this window" %}.
7 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/payments/return.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}payment return{% endblock %}
4 | {% block head %} {% endblock %}
5 | {% block content %}
6 | {% trans "Please hold" %}...
7 | ({% trans "we're waiting for PayPal" %})
8 | {% trans "If you made an eCheque payment" %} , {% trans "your account will be credited once the payment clears (so you can" %} {% trans "return to the dashboard" %} {% trans "right away" %}).
9 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/recent-sync-activity-block.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
9 |
10 |
13 |
16 |
17 |
22 |
23 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/rollback.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% load services %}
4 | {% load users %}
5 | {% load displayutils %}
6 | {% block title %}roll back activities{% endblock %}
7 |
8 | {% block content %}
9 | Roll back all activities uploaded by exercisync
10 | READ ALL OF THIS.
11 |
12 |
This page lets you roll back all activities exercisync has uploaded to most of your accounts
13 |
To emphasize, performing a roll-back will delete each and every activity that exercisync has ever uploaded, where possible. Gone with those activities will be any associated comments, pictures, kudos, KOMs, etc. - all deleted permanently .
14 |
15 |
If you deleted any of the original copies after they were synced, those activities will be lost forever .
16 |
17 |
No other activities will be deleted . exercisync tracks the unique identifiers assigned to each of its uploads by the remote service - it's these unique identifiers that are used to perform the rollback.
18 |
19 |
If the above sounds too scary , you can use the dry-run option to retrieve a list of links to the activities which would be deleted. You can then manually delete the undesired activities.
20 |
21 |
22 |
23 |
These services support rollback
24 | {% for provider in service_providers %}
25 | {% if provider.SupportsActivityDeletion %}
26 | {{ provider.DisplayName }}
27 | {% endif %}
28 | {% endfor %}
29 |
30 |
31 |
These services don't support rollback
32 | {% for provider in service_providers %}
33 | {% if not provider.SupportsActivityDeletion %}
34 |
{{ provider.DisplayName }}
35 | {% endif %}
36 | {% endfor %}
37 |
Why not? Not all services offer a method to automatically delete activities. You can still manually delete activities on these services using the activity list below.
38 |
39 |
40 |
41 |
Step 1: Retrieve list of activities (will NOT delete anything)
42 |
Fetching list (it'll take a bit)
43 |
44 |
exercisync-originating activities:
45 |
(if some activities aren't listed, make sure you're connected to all the accounts you wish to roll back. Roll-back is not available for activities uploaded prior to January 18th 2014)
46 |
47 |
{[ DisplayNameByService(svc) ]}
48 |
49 |
{[ svc ]}-{[ upload ]} See on site »
50 |
51 | {[ task.DeletionStatus[svc][upload] ? "deleted" : "delete failed" ]}
52 |
53 |
54 |
55 |
56 |
ATTEMPT DELETION OF THE ABOVE ACTIVITIES
57 |
58 |
Rollback scheduled - check list above for status
59 |
Closing your browser will not cancel the process - however you must remain on this page to receive status updates.
60 |
Deletions may fail if the activity was already deleted, or if exercisync no longer has access to your account. You can use this tool as many times as required.
61 |
62 |
63 |
64 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/service-blockingexception.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
4 |
5 | {% if exception.UserException.Type == "full" %}
6 | There wasn't enough room in your {{ provider.DisplayName }} account to synchronize activities. Once you've cleared up some space, hit the button below to re-synchronize your {{ provider.DisplayName }} account.
7 | {% elif exception.UserException.Type == "auth" %}
8 | There was a problem accessing your {{ provider.DisplayName }} account . Use the button below to reauthorize exercisync to access your account. Garmin Connect User? You may need to sign in directly on Garmin Connect and reset your password to meet new security requirements - then come back here and try again.
9 | {% elif exception.UserException.Type == "locked" or exception.UserException.Type == "reset_password" %}
10 | There was a problem accessing your {{ provider.DisplayName }} account . Visit {{ provider.DisplayName }} directly and make sure you can sign in with your username and password - there may be a problem with your account that needs to be resolved. Then, use the button below to reauthorize exercisync to access your account.
11 | {% elif exception.UserException.Type == "renew_password" %}
12 | There was a problem signing into your {{ provider.DisplayName }} account. You may have changed your password on {{ provider.DisplayName }} recently, or you need to sign into your {{ provider.DisplayName }} account directly and reset your password to meet {{ provider.DisplayName }}'s new security requirements. Once you've done so, use the button below to update your password and resume synchronization.
13 | {% elif exception.UserException.Type == "expired" %}
14 | It looks like your {{ provider.DisplayName }} account has expired . Once it's back in action, use the button below to re-synchronize your {{ provider.DisplayName }} account.
15 | {% elif exception.UserException.Type == "gc_upload_consent" %}
16 | You must grant Garmin Connect permission to accept data uploads. Once you've done so, use the button below to re-synchronize your {{ provider.DisplayName }} account.
17 | {% endif %}
18 |
19 |
32 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/service-button.html:
--------------------------------------------------------------------------------
1 | {% load services %}
2 | {% load i18n %}
3 | {% with extauth=provider.RequiresExtendedAuthorizationDetails hasextauth=connection.HasExtendedAuthorizationDetails %}
4 |
57 | {% endwith %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/settings-block.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/settings.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% load services %}
4 | {% load users %}
5 | {% load displayutils %}
6 | {% block title %}Advanced settings{% endblock %}
7 |
8 | {% block content %}
9 |
10 |
{% trans "Advanced settings" %}
11 |
52 |
53 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/site-iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | {% block content %}{% endblock %}
17 |
18 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/static/contact.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% load displayutils %}
4 | {% block title %}contact{% endblock %}
5 | {% block content %}
6 |
10 |
17 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/static/credits.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}credits{% endblock %}
4 | {% block content %}
5 | {% trans "Credits" %}
6 |
7 | {% trans "Exercisync is based on the" %} tapiriik {% trans "open source project by" %} Collin Fair
8 | {% trans "Charles Anssens" %} {% trans "for localization/internationalization and integration with DecathlonCoach" %}
9 | {% trans "Timur Bilalov" %} {% trans "for frontend contributions" %}
10 | {% trans "Aerobia, Polar, Strava et al. for creating and supporting the APIs that make this site possible" %}
11 |
12 | {% trans "and finally, all the beta testers . You know who you are." %}
13 |
14 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/static/garmin_connect_bad_data.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% block title %}tapiriik uploaded strange activities to my accounts{% endblock %}
3 | {% block content %}
4 | tapiriik uploaded strange activities to my accounts!
5 | ...what's going on?
6 |
7 |
8 |
Summary: A bug in Garmin Connect exposed test activity data used internally at Garmin. tapiriik couldn't tell these test activities weren't your own activities, so it synchronized them to your other accounts. Please delete these bad activities from all your accounts to avoid future issues.
9 |
10 |
11 |
12 |
Where did these activities come from?
13 |
On June 11, 2019, the interface which tapiriik used to download activities from Garmin Connect started to return incorrect data to tapiriik. Instead of providing a list of your own activities, it began to return test activities from an account used internally at Garmin. As tapiriik could not tell the difference between the test activities and your own, the test activities were uploaded to your other connected accounts as if they were your own.
14 |
15 |
Whose activities are they? Were my activities sent to someone else's account?
16 |
17 |
Everyone affected by this issue received the same set of test activities from the Garmin-internal test account. These test activities were were already publicly available on Garmin Connect, without any log-in required. Your activities were not sent to anyone else's accounts, nor were anyone else's activities sent to your account.
18 |
19 |
For the technically inclined, you can see a slice of the incorrect data that was being returned here (activity list) and here (details of a single activity) .
20 |
21 |
Is the issue fixed?
22 |
23 |
Yes and no. The original issue with the Garmin Connect interface has been fixed by Garmin, and additional checks have been added to tapiriik's code to prevent this situation occurring again.
24 |
25 |
However, these bad activities must now be manually deleted from your accounts so that they can not be synchronized any further. Unfortunately, tapiriik can't safely delete the bad activities for you without risking your real data, so this process must be done by hand. Sorry for the inconvenience!
26 |
27 |
Is this a problem with Strava? RunKeeper? Endomondo? etc.?
28 |
29 |
No. The issue originated with Garmin Connect and was propagated to your other accounts via tapiriik. Strava & co. were innocent bystanders to this incident.
30 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/tapiriik/web/templates/static/garmin_connect_users.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% load i18n %}
3 | {% block title %}Who is 'exercisync' on Garmin Connect{% endblock %}
4 | {% block content %}
5 | Who is 'exercisync'?
6 | (and how did they get added to my Garmin Connect account?)
7 | I'm guessing you're here because you just got an email like this:
8 |
9 |
10 |
11 |
12 | These accounts are automatically managed. You should never have to manually add or remove them as connections.
13 | Never accept a request from these users. If you do receive a connection request (different from the email above) , deny it.
14 | Your privacy settings are never changed. It's sad that this needs to be in a FAQ.
15 | You can still stop using exercisync at any time. The user will be removed from your account shortly afterwards.
16 |
17 |
18 | Here's the technical lowdown:
19 | exercisync users are quite an active bunch, but the system still spends most of its time checking accounts that haven't been updated since the last synchronization. Having these special accounts allows me to efficiently monitor which users have new activities to be synchronized, and which do not.
20 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/supported-activities.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% block title %}supported activities{% endblock %}
3 | {% block content %}
4 | {% load i18n %}
5 |
6 |
Supported activity types
7 |
8 | Activity name [tag1|tag2|…]
9 | {% with sortedActivities=actMap|dictsortreversed:"name" %}
10 | {% for activity in sortedActivities %}
11 | {{ activity.name }} [{% for synonym in activity.synonyms %}{{ synonym|safe }} {% if not forloop.last %}|{% endif %}{% endfor %}]
12 | {% endfor %}
13 | {% endwith %}
14 |
15 |
16 |
{% trans "This list doesn't matter" %}
17 |
{% trans "Do not despair if the activities you want to sync are not on this list , exercisync can still synchronize them. You'll just have to change them to the appropriate activity type once they've arrived at their final destination(s). HR, power, cadence, temperature, and caloric data will always be synchronized when the destination supports it, no matter what type of activity the data is attached to." %}
18 |
19 |
20 |
21 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templates/supported-services-poll.html:
--------------------------------------------------------------------------------
1 | {% extends "site.html" %}
2 | {% block title %}vote for new services{% endblock %}
3 | {% load displayutils %}
4 | {% block content %}
5 |
6 | {% endblock %}
--------------------------------------------------------------------------------
/tapiriik/web/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Antash/exercisync/313a81ca2c9cf53fcbdc27f8df27bf1007567bcc/tapiriik/web/templatetags/__init__.py
--------------------------------------------------------------------------------
/tapiriik/web/templatetags/displayutils.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.utils.timesince import timesince
3 | from datetime import datetime, date
4 | import json
5 | register = template.Library()
6 |
7 | @register.filter(name="utctimesince")
8 | def utctimesince(value):
9 | if not value:
10 | return ""
11 | return timesince(value, now=datetime.utcnow())
12 |
13 | @register.filter(name="fractional_hour_duration")
14 | def fractional_hour_duration(value):
15 | if value is None:
16 | return ""
17 | return "%2.f hours" % (value / 60 / 60)
18 |
19 | @register.filter(name="format_fractional_percentage")
20 | def fractional_percentage(value):
21 | try:
22 | return "%d%%" % round(value * 100)
23 | except:
24 | return "NaN"
25 |
26 | @register.filter(name="format_meters")
27 | def meters_to_kms(value):
28 | try:
29 | return round(value / 1000)
30 | except:
31 | return "NaN"
32 |
33 | @register.filter(name="format_daily_meters_hourly_rate")
34 | def meters_per_day_to_km_per_hour(value):
35 | try:
36 | return (value / 24) / 1000
37 | except:
38 | return "0"
39 |
40 | @register.filter(name="format_seconds_minutes")
41 | def meters_to_kms(value):
42 | try:
43 | return round(value / 60, 3)
44 | except:
45 | return "NaN"
46 |
47 | @register.filter(name='json')
48 | def jsonit(obj):
49 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime) or isinstance(obj, date) else None
50 | return json.dumps(obj, default=dthandler)
51 |
52 | @register.filter(name='dict_get')
53 | def dict_get(tdict, key):
54 | if type(tdict) is not dict:
55 | tdict = tdict.__dict__
56 | return tdict.get(key, None)
57 |
58 |
59 | @register.filter(name='format')
60 | def format(format, var):
61 | return format.format(var)
62 |
63 | @register.simple_tag
64 | def stringformat(value, *args):
65 | return value.format(*args)
66 |
67 | @register.filter(name="percentage")
68 | def percentage(value, *args):
69 | if not value:
70 | return "NaN"
71 | try:
72 | return str(round(float(value) * 100)) + "%"
73 | except ValueError:
74 | return value
75 |
76 |
77 | def do_infotip(parser, token):
78 | tagname, infotipId = token.split_contents()
79 | nodelist = parser.parse(('endinfotip',))
80 | parser.delete_first_token()
81 | return InfoTipNode(nodelist, infotipId)
82 |
83 | class InfoTipNode(template.Node):
84 | def __init__(self, nodelist, infotipId):
85 | self.nodelist = nodelist
86 | self.infotipId = infotipId
87 | def render(self, context):
88 | hidden_infotips = context.get('hidden_infotips', None)
89 | if hidden_infotips and self.infotipId in hidden_infotips:
90 | return ""
91 | output = self.nodelist.render(context)
92 | return "%s
" % (self.infotipId, output)
93 |
94 | register.tag("infotip", do_infotip)
--------------------------------------------------------------------------------
/tapiriik/web/templatetags/services.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from tapiriik.services import Service, ServiceRecord
3 | from tapiriik.database import db
4 | register = template.Library()
5 |
6 |
7 | @register.filter(name="svc_ids")
8 | def IDs(value):
9 | return [x["Service"] for x in value]
10 |
11 |
12 | @register.filter(name="svc_providers_except")
13 | def exceptSvc(value):
14 | connections = [y["Service"] for y in value]
15 | return [x for x in Service.List() if x.ID not in connections]
16 |
17 |
18 |
19 | @register.filter(name="svc_populate_conns")
20 | def fullRecords(conns):
21 | return [ServiceRecord(x) for x in db.connections.find({"_id": {"$in": [x["ID"] for x in conns]}})]
22 |
--------------------------------------------------------------------------------
/tapiriik/web/templatetags/users.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from tapiriik.auth import User
3 | register = template.Library()
4 |
5 |
6 | @register.filter(name="has_active_payment")
7 | def HasActivePayment(user):
8 | return User.HasActivePayment(user)
9 |
--------------------------------------------------------------------------------
/tapiriik/web/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates writing tests using the unittest module. These will pass
3 | when you run "manage.py test".
4 |
5 | Replace this with more appropriate tests for your application.
6 | """
7 |
8 | from django.test import TestCase
9 |
10 |
11 | class SimpleTest(TestCase):
12 | def test_basic_addition(self):
13 | """
14 | Tests that 1 + 1 always equals 2.
15 | """
16 | self.assertEqual(1 + 1, 2)
17 |
--------------------------------------------------------------------------------
/tapiriik/web/views.py:
--------------------------------------------------------------------------------
1 | # Create your views here.
2 |
--------------------------------------------------------------------------------
/tapiriik/web/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .dashboard import *
2 | from .diagnostics import *
3 | from .auth import *
4 | from .account import *
5 | from .sync import *
6 | from .supported_activities import *
7 | from .supported_services import *
8 | from .payments import *
9 | from .settings import *
10 | from .ab import *
11 | from .activities_dashboard import *
12 | from .rollback import *
13 | from .download import *
14 | # why did I do it this way? should make it less bad
15 |
--------------------------------------------------------------------------------
/tapiriik/web/views/ab.py:
--------------------------------------------------------------------------------
1 | from tapiriik.database import db
2 | from django.http import HttpResponse
3 | from django.views.decorators.http import require_POST
4 | import zlib
5 | from datetime import datetime
6 |
7 |
8 | _experiments = {}
9 |
10 |
11 | def ab_register_experiment(key, variants):
12 | _experiments[key] = {"Variants": variants}
13 |
14 | def ab_select_variant(key, userKey):
15 | selector = 0
16 | selector = zlib.adler32(bytes(str(key), "UTF-8"), selector)
17 | selector = zlib.adler32(bytes(str(userKey), "UTF-8"), selector)
18 | selector = selector % len(_experiments[key]["Variants"])
19 | return _experiments[key]["Variants"][selector]
20 |
21 | def ab_experiment_begin(key, userKey):
22 | db.ab_experiments.insert({"User": userKey, "Experiment": key, "Begin": datetime.utcnow(), "Variant": ab_select_variant(key, userKey)})
23 |
24 | def ab_user_experiment_begin(key, request):
25 | ab_experiment_begin(key, request.user["_id"])
26 |
27 | def ab_experiment_complete(key, userKey, result):
28 | active_experiment = db.ab_experiments.find({"User": userKey, "Experiment": key, "Result": {"$exists": False}}, {"_id": 1}).sort("Begin", -1).limit(1)[0]
29 | db.ab_experiments.update({"_id": active_experiment["_id"]}, {"$set": {"Result": result}})
30 |
31 | def ab_user_experiment_complete(key, request, result):
32 | ab_experiment_complete(key, request.user["_id"], result)
33 |
34 | @require_POST
35 | def ab_web_experiment_begin(request, key):
36 | if not request.user:
37 | return HttpResponse(status=403)
38 | if key not in _experiments:
39 | return HttpResponse(status=404)
40 | ab_user_experiment_begin(key, request)
41 | return HttpResponse()
42 |
43 | def ab_experiment_context(request):
44 | context = {}
45 | if request.user:
46 | for key in _experiments.keys():
47 | context["ab_%s_%s" % (key, ab_select_variant(key, request.user["_id"]))] = True
48 | return context
49 |
--------------------------------------------------------------------------------
/tapiriik/web/views/account.py:
--------------------------------------------------------------------------------
1 | from tapiriik.sync import Sync
2 | from django.http import HttpResponse
3 | from django.views.decorators.http import require_POST
4 | from django.shortcuts import redirect
5 | from tapiriik.auth import User
6 | import json
7 | import dateutil.parser
8 |
9 |
10 | @require_POST
11 | def account_setemail(req):
12 | if not req.user:
13 | return HttpResponse(status=403)
14 | User.SetEmail(req.user, req.POST["email"])
15 | return redirect("dashboard")
16 |
17 | @require_POST
18 | def account_settimezone(req):
19 | if not req.user:
20 | return HttpResponse(status=403)
21 | User.SetTimezone(req.user, req.POST["timezone"])
22 | return HttpResponse()
23 |
24 | @require_POST
25 | def account_setconfig(req):
26 | if not req.user:
27 | return HttpResponse(status=403)
28 | data = json.loads(req.body.decode("utf-8"))
29 | if data["sync_skip_before"] and len(data["sync_skip_before"]):
30 | data["sync_skip_before"] = dateutil.parser.parse(data["sync_skip_before"])
31 | User.SetConfiguration(req.user, data)
32 | Sync.SetNextSyncIsExhaustive(req.user, True)
33 | return HttpResponse()
--------------------------------------------------------------------------------
/tapiriik/web/views/activities_dashboard.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 | from django.http import HttpResponse
3 | from tapiriik.database import db
4 | from tapiriik.settings import WITHDRAWN_SERVICES
5 | import json
6 | import datetime
7 |
8 | def activities_dashboard(req):
9 | if not req.user:
10 | return redirect("/")
11 | return render(req, "activities-dashboard.html")
12 |
13 | def activities_fetch_json(req):
14 | if not req.user:
15 | return HttpResponse(status=403)
16 |
17 | retrieve_fields = [
18 | "Activities.Prescence",
19 | "Activities.Abscence",
20 | "Activities.Type",
21 | "Activities.Name",
22 | "Activities.StartTime",
23 | "Activities.EndTime",
24 | "Activities.Private",
25 | "Activities.Stationary",
26 | "Activities.FailureCounts"
27 | ]
28 | activityRecords = db.activity_records.find_one({"UserID": req.user["_id"]}, dict([(x, 1) for x in retrieve_fields]))
29 | if not activityRecords:
30 | return HttpResponse("[]", content_type="application/json")
31 | cleanedRecords = []
32 | for activity in activityRecords["Activities"]:
33 | # Strip down the record since most of this info isn't displayed
34 | for presence in activity["Prescence"]:
35 | del activity["Prescence"][presence]["Exception"]
36 | for abscence in activity["Abscence"]:
37 | if activity["Abscence"][abscence]["Exception"]:
38 | del activity["Abscence"][abscence]["Exception"]["InterventionRequired"]
39 | del activity["Abscence"][abscence]["Exception"]["ClearGroup"]
40 | # Don't really need these seperate at this point
41 | activity["Prescence"].update(activity["Abscence"])
42 | for svc in WITHDRAWN_SERVICES:
43 | if svc in activity["Prescence"]:
44 | del activity["Prescence"][svc]
45 | del activity["Abscence"]
46 | cleanedRecords.append(activity)
47 |
48 |
49 | dthandler = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) or isinstance(obj, datetime.date) else None
50 |
51 | return HttpResponse(json.dumps(cleanedRecords, default=dthandler), content_type="application/json")
--------------------------------------------------------------------------------
/tapiriik/web/views/aerobia/__init__.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import redirect, render
2 | from django.http import HttpResponse
3 | from tapiriik.services import Service
4 | from tapiriik.auth import User
5 | import json
6 |
7 | def browse(req):
8 | if req.user is None:
9 | return HttpResponse(status=403)
10 | path = req.GET.get("path", "")
11 | if path == "/":
12 | path = ""
13 | svcRec = User.GetConnectionRecord(req.user, "aerobia")
14 | dbSvc = Service.FromID("aerobia")
15 | dbCl = dbSvc._getClient(svcRec)
16 |
17 | folders = []
18 | result = dbCl.files_list_folder(path)
19 | while True:
20 | # There's no actual way to filter for folders only :|
21 | folders += [x.path_lower for x in result.entries if not hasattr(x, "rev")]
22 | if result.has_more:
23 | result = dbCl.files_list_folder_continue(result.cursor)
24 | else:
25 | break
26 |
27 | return HttpResponse(json.dumps(sorted(folders)), content_type='application/json')
28 |
--------------------------------------------------------------------------------
/tapiriik/web/views/dashboard.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.views.decorators.csrf import ensure_csrf_cookie
3 |
4 |
5 | @ensure_csrf_cookie
6 | def dashboard(req):
7 | return render(req, "dashboard.html")
8 |
--------------------------------------------------------------------------------
/tapiriik/web/views/download.py:
--------------------------------------------------------------------------------
1 | from tapiriik.settings import USER_DATA_FILES
2 | from django.http import HttpResponse
3 | from django.views.decorators.csrf import csrf_exempt
4 |
5 | import os.path
6 |
7 | @csrf_exempt
8 | def save_content(req, file_id):
9 | zip_filename = "{}.zip".format(file_id)
10 | file_path = os.path.join(USER_DATA_FILES, zip_filename)
11 |
12 | # if req.method == "POST":
13 | # with open(file_path, 'wb+') as destination:
14 | # for chunk in req.FILES['file'].chunks():
15 | # destination.write(chunk)
16 | # return HttpResponse(status=200)
17 | #el
18 | if req.method == "GET":
19 | if os.path.isfile(file_path):
20 | zip_file = open(file_path, 'rb')
21 | resp = HttpResponse(zip_file, content_type='application/force-download')
22 | resp['Content-Disposition'] = 'attachment; filename={}'.format(zip_filename)
23 | return resp
24 |
25 | return HttpResponse(status=404)
26 |
--------------------------------------------------------------------------------
/tapiriik/web/views/dropbox/__init__.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import redirect, render
2 | from django.http import HttpResponse
3 | from tapiriik.services import Service
4 | from tapiriik.auth import User
5 | import json
6 |
7 | def browse(req):
8 | if req.user is None:
9 | return HttpResponse(status=403)
10 | path = req.GET.get("path", "")
11 | if path == "/":
12 | path = ""
13 | svcRec = User.GetConnectionRecord(req.user, "dropbox")
14 | dbSvc = Service.FromID("dropbox")
15 | dbCl = dbSvc._getClient(svcRec)
16 |
17 | folders = []
18 | result = dbCl.files_list_folder(path)
19 | while True:
20 | # There's no actual way to filter for folders only :|
21 | folders += [x.path_lower for x in result.entries if not hasattr(x, "rev")]
22 | if result.has_more:
23 | result = dbCl.files_list_folder_continue(result.cursor)
24 | else:
25 | break
26 |
27 | return HttpResponse(json.dumps(sorted(folders)), content_type='application/json')
28 |
--------------------------------------------------------------------------------
/tapiriik/web/views/oauth/__init__.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import redirect, render
2 | from django.http import HttpResponse
3 | from django.views.decorators.http import require_POST
4 | from django.views.decorators.csrf import csrf_exempt
5 | from tapiriik.services import Service
6 | from tapiriik.auth import User
7 | import json
8 |
9 |
10 | def authredirect(req, service, level=None):
11 | svc = Service.FromID(service)
12 | return redirect(svc.GenerateUserAuthorizationURL(req.session, level))
13 |
14 |
15 | def authreturn(req, service, level=None):
16 | if ("error" in req.GET or "not_approved" in req.GET):
17 | success = False
18 | else:
19 | svc = Service.FromID(service)
20 | try:
21 | uid, authData = svc.RetrieveAuthorizationToken(req, level)
22 | except Exception as e:
23 | return render(req, "oauth-failure.html", {
24 | "service": svc,
25 | "error": str(e)
26 | })
27 | serviceRecord = Service.EnsureServiceRecordWithAuth(svc, uid, authData)
28 |
29 | # auth by this service connection
30 | existingUser = User.AuthByService(serviceRecord)
31 | # only log us in as this different user in the case that we don't already have an account
32 | if req.user is None and existingUser is not None:
33 | User.Login(existingUser, req)
34 | else:
35 | User.Ensure(req)
36 | # link service to user account, possible merge happens behind the scenes (but doesn't effect active user)
37 | User.ConnectService(req.user, serviceRecord)
38 | success = True
39 |
40 | return render(req, "oauth-return.html", {"success": 1 if success else 0})
41 |
42 |
--------------------------------------------------------------------------------
/tapiriik/web/views/privacy.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from tapiriik.services import Service
3 | from tapiriik.settings import WITHDRAWN_SERVICES, SOFT_LAUNCH_SERVICES
4 | from tapiriik.auth import User
5 | import itertools
6 |
7 | def privacy(request):
8 |
9 | OPTIN = "Opt-in "
10 | NO = "No "
11 | YES = "Yes "
12 | CACHED = "Cached "
13 | SEEBELOW = "See below"
14 |
15 | services = dict([[x.ID, {"DisplayName": x.DisplayName, "ID": x.ID}] for x in Service.List()])
16 |
17 | services["garminconnect"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
18 | services["strava"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
19 | #services["sporttracks"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
20 | services["dropbox"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":CACHED})
21 | #services["runkeeper"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
22 | services["rwgps"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
23 | #services["trainingpeaks"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
24 | #services["endomondo"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
25 | #services["motivato"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
26 | #services["nikeplus"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
27 | #services["velohero"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
28 | #services["runsense"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
29 | #services["trainerroad"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
30 | services["smashrun"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
31 | services["beginnertriathlete"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data": NO})
32 | #services["trainasone"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
33 | #services["pulsstory"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
34 | #services["setio"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
35 | #services["singletracker"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
36 | services["aerobia"].update({"email": OPTIN, "password": OPTIN, "tokens": NO, "metadata": YES, "data":NO})
37 | services["polarflow"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
38 | services["decathloncoach"].update({"email": NO, "password": NO, "tokens": YES, "metadata": YES, "data":NO})
39 | services["polarpersonaltrainer"].update({"email": YES, "password": YES, "tokens": NO, "metadata": YES, "data":NO})
40 |
41 | for svc_id in itertools.chain(WITHDRAWN_SERVICES, SOFT_LAUNCH_SERVICES):
42 | if svc_id in services:
43 | del services[svc_id]
44 |
45 | services_list = sorted(services.values(), key=lambda service: service["ID"])
46 | return render(request, "privacy.html", {"services": services_list})
47 |
--------------------------------------------------------------------------------
/tapiriik/web/views/rollback.py:
--------------------------------------------------------------------------------
1 | from tapiriik.services.rollback import RollbackTask
2 | from django.http import HttpResponse
3 | from django.views.decorators.http import require_GET
4 | from django.shortcuts import redirect, render
5 |
6 | def account_rollback_initiate(req):
7 | if not req.user:
8 | return HttpResponse(status=403)
9 |
10 | task = RollbackTask.Create(req.user)
11 |
12 | return HttpResponse(task.json())
13 |
14 | def account_rollback_status(req, task_id):
15 | if not req.user:
16 | return HttpResponse(status=403)
17 | task = RollbackTask.Get(task_id)
18 |
19 | if not task:
20 | return HttpResponse(status=404)
21 |
22 | if req.method == 'POST':
23 | task.Schedule()
24 | return HttpResponse(task.json())
25 |
26 | def rollback_dashboard(req):
27 | if not req.user:
28 | return redirect('/')
29 | return render(req, "rollback.html")
--------------------------------------------------------------------------------
/tapiriik/web/views/settings.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 | from tapiriik.auth import User
3 |
4 | def settings(request):
5 | available_settings = {
6 | "allow_activity_flow_exception_bypass_via_self":
7 | {"Title": "Route activities via",
8 | "Description": "Allows activities to flow through this service to avoid a flow exception that would otherwise prevent them arriving at a destination.",
9 | "Field": "checkbox"
10 | },
11 | "sync_private":
12 | {"Title": "Sync private activities",
13 | "Description": "By default, all activities will be synced. Unsetting this will prevent private activities being taken from this service.",
14 | "Field": "checkbox",
15 | "Available": ["strava", "runkeeper"]
16 | }
17 | }
18 | conns = User.GetConnectionRecordsByUser(request.user)
19 |
20 | for key, setting in available_settings.items():
21 | available_settings[key]["Values"] = {}
22 |
23 | for conn in conns:
24 | config = conn.GetConfiguration()
25 | for key, setting in available_settings.items():
26 | if request.method == "POST":
27 | formkey = key + "_" + conn.Service.ID
28 | if setting["Field"] == "checkbox":
29 | config[key] = formkey in request.POST
30 | available_settings[key]["Values"][conn.Service.ID] = config[key]
31 |
32 | if request.method == "POST":
33 | conn.SetConfiguration(config)
34 | if request.method == "POST":
35 | return redirect("settings_panel")
36 |
37 | return render(request, "settings.html", {"user": request.user, "settings": available_settings})
38 |
--------------------------------------------------------------------------------
/tapiriik/web/views/supported_activities.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 |
4 | def supported_activities(req):
5 | # so as not to force people to read REGEX to understand what they can name their activities
6 | ELLIPSES = "…"
7 | activities = {}
8 | activities["Running"] = ["run", "running"]
9 | activities["Cycling"] = ["cycling", "cycle", "bike", "biking"]
10 | activities["Mountain biking"] = ["mtnbiking", "mtnbiking", "mountainbike", "mountainbiking"]
11 | activities["Walking"] = ["walking", "walk"]
12 | activities["Hiking"] = ["hike", "hiking"]
13 | activities["Downhill skiing"] = ["downhill", "downhill skiing", "downhill-skiing", "downhillskiing", ELLIPSES]
14 | activities["Cross-country skiing"] = ["xcskiing", "xc-skiing", "xc-ski", "crosscountry-skiing", ELLIPSES]
15 | activities["Roller skiing"] = ["rollerskiing"]
16 | activities["Snowboarding"] = ["snowboarding", "snowboard"]
17 | activities["Skating"] = ["skate", "skating"]
18 | activities["Swimming"] = ["swim", "swimming"]
19 | activities["Wheelchair"] = ["wheelchair"]
20 | activities["Rowing"] = ["rowing", "row"]
21 | activities["Elliptical"] = ["elliptical"]
22 | activities["Climbing"] = ["climb", "climbing"]
23 | activities["Strength Training"] = ["strength", "strength training"]
24 | activities["Gym"] = ["gym", "workout"]
25 | activities["Stand-up paddling"] = ["sup", "stand-up paddling", "standup paddling", "standuppaddling"]
26 | activities["Other"] = ["other", "unknown"]
27 | activityList = []
28 | for act, synonyms in activities.items():
29 | activityList.append({"name": act, "synonyms": synonyms})
30 | return render(req, "supported-activities.html", {"actMap": activityList})
31 |
--------------------------------------------------------------------------------
/tapiriik/web/views/supported_services.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 |
3 | def supported_services_poll(req):
4 | return render(req, "supported-services-poll.html", {"voter_key": req.user["_id"] if req.user else ""}) # Should probably do something with ancestor accounts?
--------------------------------------------------------------------------------
/tapiriik/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for tapiriik project.
3 |
4 | This module contains the WSGI application used by Django's development server
5 | and any production WSGI deployments. It should expose a module-level variable
6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
7 | this application via the ``WSGI_APPLICATION`` setting.
8 |
9 | Usually you will have the standard Django WSGI application here, but it also
10 | might make sense to replace the whole Django WSGI application with a custom one
11 | that later delegates to the Django one. For example, you could introduce WSGI
12 | middleware here, or combine a Django application with an application of another
13 | framework.
14 |
15 | """
16 | import os
17 |
18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
19 | # if running multiple sites in the same mod_wsgi process. To fix this, use
20 | # mod_wsgi daemon mode with each site in its own daemon process, or use
21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "tapiriik.settings"
22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tapiriik.settings")
23 |
24 | # This application object is used by any WSGI server configured to use this
25 | # file. This includes Django's development server, if the WSGI_APPLICATION
26 | # setting points here.
27 | from django.core.wsgi import get_wsgi_application
28 | application = get_wsgi_application()
29 |
30 | # Apply WSGI middleware here.
31 | # from helloworld.wsgi import HelloWorldApplication
32 | # application = HelloWorldApplication(application)
33 |
--------------------------------------------------------------------------------
/tz_ingest.py:
--------------------------------------------------------------------------------
1 | # This file isn't called in normal operation, just to update the TZ boundary DB.
2 | # Should be called with `tz_world.*` files from http://efele.net/maps/tz/world/ in the working directory.
3 | # Requires pyshp and shapely for py3k (from https://github.com/mwtoews/shapely/tree/py3)
4 |
5 | import shapefile
6 | from shapely.geometry import Polygon, mapping
7 | import pymongo
8 | from tapiriik.database import tzdb
9 |
10 | print("Dropping boundaries collection")
11 | tzdb.drop_collection("boundaries")
12 |
13 | print("Setting up index")
14 | tzdb.boundaries.ensure_index([("Boundary", pymongo.GEOSPHERE)])
15 |
16 | print("Reading shapefile")
17 | records = []
18 | sf = shapefile.Reader("tz_world.shp")
19 | shapeRecs = sf.shapeRecords()
20 |
21 | ct = 0
22 | total = len(shapeRecs)
23 | for shape in shapeRecs:
24 | tzid = shape.record[0]
25 | print("%3d%% %s" % (round(ct * 100 / total), tzid))
26 | ct += 1
27 | polygon = Polygon(list(shape.shape.points))
28 | if not polygon.is_valid:
29 | polygon = polygon.buffer(0) # Resolves issues with most self-intersecting geometry
30 | assert polygon.is_valid
31 | record = {"TZID": tzid, "Boundary": mapping(polygon)}
32 | tzdb.boundaries.insert(record) # Would be bulk insert, but that makes it a pain to debug geometry issues
33 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require("path")
2 | var webpack = require('webpack')
3 | var BundleTracker = require('webpack-bundle-tracker')
4 |
5 | module.exports = {
6 | context: __dirname,
7 |
8 | entry: {
9 | exercisyncApp: './tapiriik/frontend/app',
10 | },
11 |
12 | output: {
13 | path: path.resolve('./tapiriik/web/static/js/bundles/'),
14 | filename: "[name].js"
15 | },
16 |
17 | plugins: [
18 | new BundleTracker({ filename: './webpack-stats.json' }),
19 | new webpack.HotModuleReplacementPlugin()
20 | ],
21 |
22 | module: {
23 | rules: [
24 | {
25 | test: /\.jsx?$/,
26 | exclude: /node_modules/,
27 | use: ['babel-loader']
28 | },
29 | {
30 | test: /\.css$/,
31 | loader: 'style-loader!css-loader'
32 | },
33 | ],
34 | },
35 |
36 | resolve: {
37 | modules: ['node_modules'],
38 | extensions: ['.js', '.jsx']
39 | }
40 | }
--------------------------------------------------------------------------------