├── .codeclimate.yml ├── .eslintrc ├── .github ├── CONTRIBUTING.md └── LICENSE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── ISSUES.md ├── LICENSE ├── README.md ├── actions ├── __init__.py └── adminActions.py ├── api.py ├── app.yaml ├── appengine_config.py ├── authorized.py ├── common ├── __init__.py ├── aes_cypher.py ├── decorators.py └── my_filters.py ├── constants.py ├── cron.yaml ├── design ├── AppleTouchIcon.sketch ├── banner.png ├── banner.psd ├── logo.psd ├── logo_1024.png ├── logo_1024_white.png ├── logo_128.png ├── logo_128_white.png ├── logo_192.png ├── logo_512.png ├── play_store_feature.png └── screenshot_frame.psd ├── django_version.py ├── flow.py ├── gulpfile.js ├── handlers.py ├── images ├── gassistant_512.png ├── logo_120.png ├── logo_128.png ├── logo_white.png ├── messenger_512.png ├── puff.svg └── screenshots │ ├── analysis.png │ ├── android_snapshot_1.png │ ├── android_snapshot_2.png │ ├── dashboard.png │ ├── habit.png │ ├── habit_trend.png │ ├── snapshots.png │ └── timeline.png ├── index.yaml ├── local.requirements.txt ├── models.py ├── package.json ├── pytz ├── __init__.py ├── gae.py ├── reference.py ├── tzfile.py ├── tzinfo.py └── zoneinfo.zip ├── queue.yaml ├── reports.py ├── requirements.txt ├── robots.txt ├── scripts ├── deploy.sh ├── fetch_gae_sdk.py ├── run_tests.sh ├── runtests.py └── server.sh ├── services ├── __init__.py ├── agent.py ├── flow_bigquery.py ├── flow_evernote.py ├── gfit.py ├── github.py ├── goodreads.py ├── gservice.py └── pocket.py ├── settings ├── __init__.py └── secrets_template.py ├── src ├── error.html ├── index.html └── js │ ├── __tests__ │ └── util-test.js │ ├── actions │ ├── ProjectActions.js │ ├── TaskActions.js │ └── UserActions.js │ ├── components │ ├── About.js │ ├── Analysis.js │ ├── App.js │ ├── Auth.js │ ├── Dashboard.js │ ├── Feedback.js │ ├── FlashCard.js │ ├── GoalViewer.js │ ├── HabitAnalysis.js │ ├── HabitHistory.js │ ├── HabitWidget.js │ ├── Integrations.js │ ├── JournalEditor.js │ ├── JournalHistory.js │ ├── MiniJournalWidget.js │ ├── NotFound.js │ ├── Privacy.js │ ├── ProjectAnalysis.js │ ├── ProjectHistory.js │ ├── ProjectViewer.js │ ├── ReadWidget.js │ ├── Reading.js │ ├── ReadingDetail.js │ ├── Reports.js │ ├── Settings.js │ ├── Site.js │ ├── Splash.js │ ├── TaskHUD.js │ ├── TaskHistory.js │ ├── TaskWidget.js │ ├── Timeline.js │ ├── TrackingHistory.js │ ├── admin │ │ └── AdminAgent.js │ ├── analysis │ │ ├── AnalysisGoals.js │ │ ├── AnalysisHabits.js │ │ ├── AnalysisJournals.js │ │ ├── AnalysisMisc.js │ │ ├── AnalysisSnapshot.js │ │ └── AnalysisTasks.js │ ├── common │ │ ├── AsyncActionButton.js │ │ ├── BigProp.js │ │ ├── DateTime.js │ │ ├── EntityMap.js │ │ ├── FetchedList.js │ │ ├── GoogleLoginCompat.js │ │ ├── LoadStatus.js │ │ ├── MobileDialog.js │ │ ├── ProgressLine.js │ │ ├── ReactJsonEditor.js │ │ └── YearSelector.js │ └── list_items │ │ ├── JournalLI.js │ │ ├── ProjectLI.js │ │ ├── QuoteLI.js │ │ ├── ReadableLI.js │ │ └── TaskLI.js │ ├── config │ ├── Routes.js │ ├── alt.js │ └── history.js │ ├── constants │ ├── AppConstants.js │ ├── Styles.js │ └── client_secrets.template.js │ ├── main.js │ ├── sources │ └── ProjectSource.js │ ├── stores │ ├── ProjectStore.js │ ├── TaskStore.js │ └── UserStore.js │ └── utils │ ├── action-utils.js │ ├── api.js │ ├── component-utils.js │ ├── store-utils.js │ └── util.js ├── static ├── apple-touch-icon.png ├── bootstrap │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── npm.js ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── flow-agent │ ├── agent.json │ ├── customDomainsResponses.json │ └── intents │ │ ├── Add Task.json │ │ ├── Daily Journal.json │ │ ├── Default Fallback Intent.json │ │ ├── Disconnect.json │ │ ├── Flow Status Request.json │ │ ├── Goals Help.json │ │ ├── Goals Request.json │ │ ├── Habit Request.json │ │ ├── Habit or Task Report.json │ │ ├── Habits Help.json │ │ ├── Help.json │ │ ├── Report Habit Commitment.json │ │ ├── Task Request.json │ │ ├── Tasks Help.json │ │ └── Welcome.json ├── flow.css ├── icons.css ├── manifest.json ├── react-select.css ├── sounds │ ├── beep.mp3 │ ├── commit.mp3 │ └── complete.mp3 ├── toastr.css └── toastr.min.css ├── swagger.yaml ├── tasks.py ├── testing ├── __init__.py ├── base_test_case.py ├── readme.txt ├── testing_agent.py ├── testing_apiai_requests.py ├── testing_apis.py ├── testing_authentication.py ├── testing_facebook_requests.py ├── testing_goals.py ├── testing_habits.py ├── testing_journaling.py ├── testing_projects.py ├── testing_readables.py ├── testing_reports.py ├── testing_snapshots.py ├── testing_users.py └── testing_util.py ├── tools.py └── views ├── __init__.py └── views.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | csslint: 4 | enabled: true 5 | duplication: 6 | enabled: true 7 | config: 8 | languages: 9 | - javascript 10 | - python 11 | eslint: 12 | enabled: true 13 | config: 14 | extensions: 15 | - .es6 16 | fixme: 17 | enabled: true 18 | radon: 19 | enabled: true 20 | checks: 21 | Complexity: 22 | enabled: false 23 | config: 24 | python_version: 2 25 | ratings: 26 | paths: 27 | - "**.css" 28 | - "**.inc" 29 | - "**.js" 30 | - "**.jsx" 31 | - "**.module" 32 | - "**.py" 33 | exclude_paths: 34 | - "tests/" 35 | - "testing/" 36 | - "static/" 37 | - "pytz/" -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "eslint:recommended", "plugin:react/recommended" ], 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "parser": "babel-eslint", 8 | "rules": { 9 | "strict": 0 10 | }, 11 | "parserOptions": { 12 | ecmaVersion: 6, 13 | ecmaFeatures: { 14 | "jsx": true 15 | }, 16 | sourceType: "module" 17 | }, 18 | "plugins": ["react"] 19 | } -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in Flow Dashboard. All forms of contribution are 4 | welcome, from issue reports to PRs. 5 | 6 | * We use node.js v3 for development and testing. 7 | 8 | ## Code structure 9 | 10 | Key files: 11 | 12 | * `models.py` - All db model definitions, most with Update() and Fetch() methods 13 | * `flow.py` - WSGI app setup and route lookup for all handlers and API calls 14 | * `api.py` - All API calls 15 | * `Routes.js` - Core react-router routes /app etc 16 | * `App.js` - Component for main app frame, renders all sub-routes as children 17 | * `Dashboard.js` - Dashboard component, renders each dashboard widget, etc 18 | 19 | ## Before you open a PR 20 | 21 | * See README.md for setup instructions 22 | * Make sure there's an issue open for any work you take on and intend to submit 23 | as a pull request - it helps to review your concept and direction 24 | early and is a good way to discuss what you're planning to do. 25 | * If you open an issue and are interested in working on a fix, please let us 26 | know. We'll help you get started, rather than adding it to the queue. 27 | * Where possible, include tests with your changes, either that demonstrates the 28 | bug, or tests the new functionality. If you're not sure how to test your 29 | changes, feel free to ping @onejgordon 30 | 31 | -------------------------------------------------------------------------------- /.github/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011-2017 Jeremy Gordon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | node_modules/* 3 | *.swp 4 | lib/* 5 | !lib/__init___.py 6 | *.DS_Store 7 | .DS_Store 8 | *.pyc 9 | dist/* 10 | secrets.py 11 | src/js/constants/client_secrets.js 12 | client_secret.json 13 | settings/*.json 14 | env/* 15 | bin/* 16 | include/* 17 | .Python 18 | client_secret.json 19 | .coverage 20 | yarn-error.log 21 | package-lock.json 22 | yarn.lock 23 | genzai-app-*.json 24 | .python-version 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use Dockerized infrastructure 2 | sudo: false 3 | language: python 4 | python: 2.7 5 | # Cache our Gcloud SDK between commands 6 | cache: 7 | directories: 8 | - "$HOME/google-cloud-sdk/" 9 | env: 10 | # Make sure App Engine SDK is in the Python path 11 | - GAE_PYTHONPATH=${HOME}/.cache/google_appengine PATH=$PATH:${HOME}/google-cloud-sdk/bin PYTHONPATH=${PYTHONPATH}:${GAE_PYTHONPATH} CLOUDSDK_CORE_DISABLE_PROMPTS=1 12 | before_install: 13 | # Install Google App Engine Python SDK 14 | - if [ ! -d "${GAE_PYTHONPATH}" ]; then 15 | python scripts/fetch_gae_sdk.py $(dirname "${GAE_PYTHONPATH}"); 16 | fi 17 | install: 18 | # Install the Python dependencies 19 | - pip install -t lib -r requirements.txt 20 | - pip install -r local.requirements.txt 21 | - pip install coveralls 22 | script: 23 | # Run the unit tests & end to end tests 24 | - coverage run --omit="runtests.py,testing/*,pytz/*,lib/*,*/site-packages/*,*/google_appengine/*,*/virtualenv/*,actions/*" scripts/runtests.py ${GAE_PYTHONPATH} testing/ all 25 | after_success: 26 | coveralls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Flow Dashboard Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Version 0-27 6 | 7 | ### Added 8 | 9 | * Define common tasks in task settings to easily add batches of common/recurring tasks 10 | * Reverse-valued journal questions for easier trend analysis on journal history 11 | * Ability to delete events 12 | * Edit title & author of books/articles 13 | 14 | ## Version 0-26 15 | 16 | ### Added 17 | 18 | * Task history viewer 19 | * Task timing (pomodoro sessions) 20 | * Optional task linking with projects 21 | * User-definable goal slots (can be increased to 10) 22 | * Individual goal assessments (averaged to overall assessment) 23 | * Drill-down on goal analysis to view goal assessments 24 | * Show points on journal analysis chart for journals including selected hashtag or mention 25 | * Ability to edit tasks 26 | * Ability to edit habits in dashboard 27 | * Support up to 10 active habits (limit increased) 28 | * Sort habits by name 29 | * Clickable shortened links in task list 30 | * Optionally define habits as countable with daily target 31 | 32 | ## Version 0-25 33 | 34 | ### Added 35 | 36 | * Clearer invalid response guidance from agent for journals 37 | * Post-dating and editing historical journals 38 | * Viewing historical journal entries 39 | * Ongoing events support in event timeline 40 | * Support for full articles clipped via Evernote 41 | 42 | ### Fixed 43 | 44 | * Timezone-dependent off-by-one issue in birthday and event date saving 45 | 46 | -------------------------------------------------------------------------------- /ISSUES.md: -------------------------------------------------------------------------------- 1 | # ISSUES 2 | 3 | 1. `ModuleNotFoundError: No module named 'jwt'` 4 | 5 | Likely cause: PyJWT not installed properly in current environment. If using conda, use `conda install PyJWT` 6 | 7 | 2. `ReferenceError: primordials is not defined` 8 | 9 | Check node version (tested with v11.15.0). Recommend using `nvm`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 onejgordon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/actions/__init__.py -------------------------------------------------------------------------------- /actions/adminActions.py: -------------------------------------------------------------------------------- 1 | import django_version 2 | from datetime import datetime 3 | from models import Quote, Goal, User, Habit, Project, Readable, Task, MiniJournal, HabitDay 4 | import authorized 5 | import handlers 6 | from google.appengine.ext import ndb 7 | 8 | 9 | class Init(handlers.BaseRequestHandler): 10 | @authorized.role("admin") 11 | def get(self, d): 12 | # Run after admin user logs in 13 | u = User.query().get() 14 | if u: 15 | today = datetime.today() 16 | g = Goal.CreateMonthly(u, date=today) 17 | g.Update(text=["Get it done"]) 18 | g2 = Goal.Create(u, str(today.year)) 19 | g2.Update(text=["Make progress"]) 20 | ndb.put_multi([g, g2]) 21 | h = Habit.Create(u) 22 | h.Update(name="Run") 23 | h.put() 24 | p = Project.Create(u) 25 | p.Update(title="Blog post", subhead="How Flow works") 26 | p.put() 27 | 28 | Task.Create(u, "Get this done").put() 29 | 30 | t = Task.Create(u, "Think hard", due=datetime.today()) 31 | t2 = Task.Create(u, "Think even harder", due=datetime.today()) 32 | message = "OK" 33 | else: 34 | message = "No user" 35 | self.json_out({'message': message}) 36 | 37 | 38 | class Hacks(handlers.JsonRequestHandler): 39 | @authorized.role("admin") 40 | def get(self, d): 41 | hack_id = self.request.get('hack_id') 42 | res = {} 43 | if hack_id == 'index_quotes_readables': 44 | page = self.request.get_range('page') 45 | PAGE_SIZE = 50 46 | index_lookup = {} # index_name -> (index, list of items) 47 | for q in Quote.query().fetch(limit=PAGE_SIZE, offset=page * PAGE_SIZE): 48 | sd, index = q.update_sd(index_put=False) 49 | if index and index.name not in index_lookup: 50 | index_lookup[index.name] = (index, [sd]) 51 | else: 52 | index_lookup[index.name][1].append(sd) 53 | for r in Readable.query().fetch(limit=PAGE_SIZE, offset=page * PAGE_SIZE): 54 | sd, index = r.update_sd(index_put=False) 55 | if index and index.name not in index_lookup: 56 | index_lookup[index.name] = (index, [sd]) 57 | else: 58 | index_lookup[index.name][1].append(sd) 59 | if index_lookup: 60 | n = 0 61 | for index_tuple in index_lookup.values(): 62 | index, items = index_tuple 63 | index.put(items) 64 | n += len(items) 65 | res['result'] = "Put %d items in %d indexes" % (n, len(index_tuple)) 66 | res['page'] = page 67 | 68 | elif hack_id == 'normalize_key_props': 69 | dbp = [] 70 | for hd in HabitDay.query().iter(): 71 | habit_key = hd.habit 72 | if habit_key.parent() is None: 73 | # Need to update 74 | hd.habit = ndb.Key('User', hd.key.parent().id(), 'Habit', int(habit_key.id())) 75 | dbp.append(hd) 76 | res['habitdays'] = len(dbp) 77 | ndb.put_multi(dbp) 78 | dbp = [] 79 | for jrnl in MiniJournal.query().iter(): 80 | changes = False 81 | for i, tag_key in enumerate(jrnl.tags): 82 | if tag_key.parent() is None: 83 | # Need to update 84 | jrnl.tags[i] = ndb.Key('User', jrnl.key.parent().id(), 'JournalTag', tag_key.id()) 85 | changes = True 86 | if changes: 87 | dbp.append(jrnl) 88 | res['journals'] = len(dbp) 89 | ndb.put_multi(dbp) 90 | 91 | else: 92 | res['result'] = 'hack_id not found' 93 | self.json_out(res) 94 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | default_expiration: "1d" 6 | 7 | automatic_scaling: 8 | max_instances: 1 9 | max_pending_latency: 200ms # default 30ms 10 | 11 | inbound_services: 12 | - warmup 13 | 14 | handlers: 15 | - url: /favicon.ico 16 | static_files: static/favicon.ico 17 | upload: static/favicon\.ico 18 | 19 | - url: /(.*\.(appcache|manifest)) 20 | mime_type: text/cache-manifest 21 | static_files: static/\1 22 | upload: static/(.*\.(appcache|manifest)) 23 | expiration: "0m" 24 | 25 | - url: /images 26 | static_dir: images 27 | expiration: "99d" 28 | 29 | - url: /dist/src 30 | static_dir: dist/src 31 | 32 | - url: /dist/build 33 | static_dir: dist/build 34 | 35 | - url: /static 36 | static_dir: static 37 | expiration: "99d" 38 | 39 | - url: /cron/.* 40 | script: flow.app 41 | login: admin 42 | 43 | - url: /admin/gauth.* 44 | script: flow.app 45 | login: admin 46 | secure: always 47 | 48 | - url: /humans.txt 49 | static_files: humans.txt 50 | upload: humans.txt 51 | 52 | - url: /robots.txt 53 | static_files: robots.txt 54 | upload: robots.txt 55 | 56 | - url: /js 57 | static_dir: js 58 | 59 | - url: /tasks/ 60 | script: flow.app 61 | login: admin 62 | 63 | - url: /.* 64 | script: flow.app 65 | secure: always 66 | 67 | builtins: 68 | - deferred: on 69 | - remote_api: on 70 | 71 | skip_files: 72 | - ^(.*/)?#.*#$ 73 | - ^(.*/)?.*~$ 74 | - ^(.*/)?.*\.py[co]$ 75 | - ^(.*/)?.*\.scss$ 76 | - ^(.*/)?.*\.less$ 77 | - ^(.*/)?.*/RCS/.*$ 78 | - ^(.*/)?\..*$ 79 | - ^(node_modules/.*) 80 | - .*node_modules 81 | - ^design/.*$ 82 | - ^lib/.*(\.(?!py)).*$ 83 | - ^scripts/.*$ 84 | - ^env/.*$ 85 | - ^\.Python$ 86 | - ^bin/.*$ 87 | 88 | libraries: 89 | - name: django 90 | version: "1.2" 91 | - name: webapp2 92 | version: "2.5.1" 93 | - name: jinja2 94 | version: "2.6" 95 | - name: lxml 96 | version: "2.3.5" 97 | - name: pycrypto 98 | version: "2.6" 99 | 100 | env_variables: 101 | GOOGLE_APPLICATION_CREDENTIALS: 'settings/genzai-app-f02b0e933bb4.json' 102 | -------------------------------------------------------------------------------- /appengine_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from google.appengine.ext import vendor 5 | import os 6 | import logging 7 | 8 | USE_ABSOLUTE = True 9 | LIBDIR = 'lib' 10 | # Add any libraries installed in the "lib" folder. 11 | if USE_ABSOLUTE: 12 | lib_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), LIBDIR) 13 | else: 14 | lib_path = LIBDIR 15 | vendor.add(lib_path) 16 | 17 | logging.debug("Ran appengine_config, lib_path: %s" % lib_path) 18 | -------------------------------------------------------------------------------- /authorized.py: -------------------------------------------------------------------------------- 1 | """ 2 | authorized.py 3 | 4 | """ 5 | 6 | import django_version 7 | from constants import SITENAME, TAGLINE, AUTHOR_NAME 8 | from datetime import datetime 9 | import base64 10 | from models import User 11 | import logging 12 | 13 | 14 | def role(role=None): 15 | def wrapper(handler_method): 16 | def check_login(self, *args, **kwargs): 17 | d = { 18 | 'SITENAME': SITENAME, 19 | 'TAGLINE': TAGLINE, 20 | 'AUTHOR_NAME': AUTHOR_NAME, 21 | 'YEAR': datetime.now().year, 22 | 'CURTIME': datetime.now() 23 | } 24 | allow = False 25 | handled = False 26 | user = None 27 | session = self.session 28 | if 'user' in session: 29 | user = session['user'] 30 | if not user and role: 31 | headers = self.request.headers 32 | if headers: 33 | authorization = headers.get('authorization') 34 | if authorization and authorization.startswith("Basic "): 35 | auth_b64 = authorization.replace('Basic ','') 36 | user_pass = base64.b64decode(auth_b64) 37 | if user_pass: 38 | _user_id, _pass = user_pass.split(':') 39 | if _user_id and _pass: 40 | if _user_id.isdigit(): 41 | # Interpret as User ID 42 | user = User.get_by_id(int(_user_id)) 43 | elif '@' in _user_id: 44 | # Interpret as user email 45 | user = User.GetByEmail(_user_id) 46 | if user and not user.checkPass(_pass): 47 | user = None 48 | if not role: 49 | allow = True 50 | elif role == "user": 51 | if user: 52 | allow = True 53 | elif role == "admin": 54 | if user and user.admin(): 55 | allow = True 56 | if not handled: 57 | if allow: 58 | self.user = d['user'] = user 59 | d['logout_url'] = "/logout" 60 | kwargs['d'] = d 61 | handler_method(self, *args, **kwargs) 62 | else: 63 | # Unauthorized 64 | self.set_response(success=False, message="Unauthorized", status=401) 65 | 66 | return check_login 67 | return wrapper 68 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/common/__init__.py -------------------------------------------------------------------------------- /common/aes_cypher.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Crypto.Cipher import AES 3 | from Crypto import Random 4 | 5 | 6 | class AESCipher: 7 | def __init__(self, key): 8 | self.key = key 9 | self.BS = 16 10 | 11 | def pad(self, s): 12 | return s + (self.BS - len(s) % self.BS) * chr(self.BS - len(s) % self.BS) 13 | 14 | def unpad(self, s): 15 | return s[:-ord(s[len(s)-1:])] 16 | 17 | def encrypt(self, raw): 18 | raw = self.pad(raw) 19 | iv = Random.new().read(AES.block_size) 20 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 21 | return base64.b64encode(iv + cipher.encrypt(raw)) 22 | 23 | def decrypt(self, enc): 24 | enc = base64.b64decode(enc) 25 | iv = enc[:16] 26 | cipher = AES.new(self.key, AES.MODE_CBC, iv) 27 | return self.unpad(cipher.decrypt(enc[16:])) 28 | -------------------------------------------------------------------------------- /common/decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import functools 4 | import logging 5 | from time import time 6 | from google.appengine.api import memcache 7 | import tools 8 | import os 9 | 10 | NOTIFY_FAIL_RETRY_COUNT = 5 11 | PERMANENT_FAIL_RETRY_COUNT = 15 12 | 13 | 14 | def deferred_task_decorator(method): 15 | @functools.wraps(method) 16 | def defer_method(*args, **kwargs): 17 | # Collecting defered task header information 18 | headers = {} 19 | headers["queue_name"] = os.environ.get('HTTP_X_APPENGINE_QUEUENAME', '') 20 | headers["retry_count"] = os.environ.get('HTTP_X_APPENGINE_TASKRETRYCOUNT', 0) 21 | headers["task_name"] = os.environ.get('HTTP_X_APPENGINE_TASKNAME', '') 22 | 23 | if not tools.on_dev_server(): 24 | logging.info("deferred_task_decorator : {}".format(headers)) 25 | # Running for the first time ignore 26 | _retry_count = headers.get("retry_count", 0) 27 | retry_count = int(_retry_count) if tools.safeIsDigit(_retry_count) else 0 28 | if retry_count < 1: 29 | return method(*args, **kwargs) 30 | 31 | # We are retying 32 | if not tools.on_dev_server(): 33 | logging.info("Found task retry count ok ..{}:{} - ({})" 34 | .format(headers.get("queue_name"), headers.get("task_name"), retry_count)) 35 | 36 | return method(*args, **kwargs) 37 | 38 | return defer_method 39 | 40 | 41 | def auto_cache(expiration=60*60, key=None): 42 | """ 43 | A decorator to memorize the results of a function call in memcache. Use this 44 | in preference to doing your own memcaching, as this function avoids version 45 | collisions etc... 46 | Note that if you are not providing a key (or a function to create one) then your 47 | arguments need to provide consistent str representations of themselves. Without an 48 | implementation you could get the memory address as part of the result - "<... object at 0x1aeff0>" 49 | which is going to vary per request and thus defeat the caching. 50 | 51 | Usage: 52 | @auto_cache 53 | get_by_type(type): 54 | return MyModel.all().filter("type =", type) 55 | 56 | :param expiration: Number of seconds before the value is forced to re-cache, 0 57 | for indefinite caching 58 | 59 | :param key: Option manual key, use in combination with expiration=0 to have 60 | memcaching with manual updating (eg by cron job). Key can be a func(*args, **kwargs) 61 | :rtype: Memoized return value of function 62 | """ 63 | 64 | def wrapper(fn): 65 | @functools.wraps(fn) 66 | def cache_decorator(*args, **kwargs): 67 | mc_key = None 68 | force_refresh = kwargs.pop('refresh', False) 69 | if key: 70 | if callable(key): 71 | mc_key = key(*args, **kwargs) 72 | else: 73 | mc_key = key 74 | else: 75 | mc_key = '%s:%s' % ( 76 | "auto_cache", 77 | tools.make_function_signature( 78 | fn.func_name, *args, **kwargs)) 79 | 80 | if force_refresh: 81 | # Force refresh, dont get from memcache 82 | result = None 83 | else: 84 | result = memcache.get(mc_key) 85 | if result == "MCDUMMY": 86 | result = None 87 | if result is not None: 88 | pass 89 | else: 90 | result = fn(*args, **kwargs) 91 | try: 92 | memcache.set(mc_key, result, time=expiration) 93 | except ValueError, e: 94 | logging.critical( 95 | "Recevied error from memcache", exc_info=e) 96 | return result 97 | return cache_decorator 98 | return wrapper 99 | -------------------------------------------------------------------------------- /common/my_filters.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import jinja2 3 | import json 4 | 5 | 6 | def printjson(d): 7 | if d: 8 | return jinja2.Markup(json.dumps(d)) 9 | else: 10 | return 'null'; 11 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | 3 | - description: sync with pocket & goodreads 4 | url: /cron/readables/sync 5 | schedule: every day 20:30 6 | 7 | - description: sync from google fit 8 | url: /cron/pull/google_fit 9 | schedule: every day 03:00 10 | 11 | - description: sync with github 12 | url: /cron/pull/github 13 | schedule: every day 00:00 14 | 15 | - description: Delete reports older than 30 days 16 | url: /cron/reports/delete_old 17 | schedule: every sunday 00:00 18 | 19 | - description: push to bigquery 20 | url: /cron/push/bigquery 21 | schedule: every saturday 00:00 22 | -------------------------------------------------------------------------------- /design/AppleTouchIcon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/AppleTouchIcon.sketch -------------------------------------------------------------------------------- /design/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/banner.png -------------------------------------------------------------------------------- /design/banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/banner.psd -------------------------------------------------------------------------------- /design/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo.psd -------------------------------------------------------------------------------- /design/logo_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo_1024.png -------------------------------------------------------------------------------- /design/logo_1024_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo_1024_white.png -------------------------------------------------------------------------------- /design/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo_128.png -------------------------------------------------------------------------------- /design/logo_128_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo_128_white.png -------------------------------------------------------------------------------- /design/logo_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo_192.png -------------------------------------------------------------------------------- /design/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/logo_512.png -------------------------------------------------------------------------------- /design/play_store_feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/play_store_feature.png -------------------------------------------------------------------------------- /design/screenshot_frame.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/design/screenshot_frame.psd -------------------------------------------------------------------------------- /django_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ["DJANGO_SETTINGS_MODULE"] = "settings" 3 | 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var source = require('vinyl-source-stream'); 3 | var buffer = require('vinyl-buffer'); 4 | var browserify = require('browserify'); 5 | var watchify = require('watchify'); 6 | var babelify = require('babelify'); 7 | var uglify = require('gulp-uglify'); 8 | var sourcemaps = require('gulp-sourcemaps'); 9 | var log = require('fancy-log'); 10 | var resolutions = require('browserify-resolutions'); 11 | var hash_src = require("gulp-hash-src"); 12 | var assign = require('lodash.assign'); 13 | 14 | var path = { 15 | HTML: ['src/{*.html,*.xml}'], 16 | HTML_INDEX: 'src/index.html', 17 | OUT: 'build.js', 18 | DEST: 'dist', 19 | BROWSERIFY_PATHS: ['./src/js','./node_modules'], 20 | DEST_SRC: 'dist/src', 21 | ENTRY_POINT: './src/js/main.js' 22 | }; 23 | 24 | path.WATCH_DIRS = path.HTML 25 | 26 | var BROWSERIFY_OPTS = { 27 | entries: [path.ENTRY_POINT], 28 | debug: true, 29 | paths: path.BROWSERIFY_PATHS, 30 | cache: {}, 31 | packageCache: {}, 32 | fullPaths: true 33 | }; 34 | 35 | gulp.task('copy', function(){ 36 | gulp.src(path.HTML, {base: 'src/'}) 37 | .pipe(gulp.dest(path.DEST)); 38 | }); 39 | 40 | var bopts = assign({}, watchify.args, BROWSERIFY_OPTS); 41 | var b = browserify(bopts) 42 | .plugin(resolutions, 'react') 43 | .transform(babelify.configure({ 44 | optional: ["es7.decorators","es7.asyncFunctions","es7.classProperties"], 45 | experimental: true 46 | }) 47 | ); 48 | 49 | gulp.task('watch', function() { 50 | var w = watchify(b); 51 | w.on('update', bundle); // on any dep update, runs the bundler 52 | w.on('log', log); // output build logs to terminal 53 | 54 | var dirs = path.WATCH_DIRS.concat(['src/js/**/*.js']); 55 | gulp.watch(dirs, ['copy']); 56 | }); 57 | 58 | 59 | gulp.task('build_bundle', bundle); 60 | 61 | function bundle() { 62 | var now = new Date(); 63 | log(now + ' - built bundle'); 64 | 65 | var stream = b.bundle().on('error', function(err){ 66 | console.log(err.message); 67 | this.emit('end'); 68 | }) 69 | .pipe(source(path.OUT)) 70 | .pipe(buffer()) 71 | .pipe(sourcemaps.init({ loadMaps: true })); 72 | 73 | if (process.env.NODE_ENV === 'production') { 74 | stream = stream.pipe(uglify()); 75 | } 76 | 77 | return ( 78 | stream 79 | .pipe(sourcemaps.write('./')) 80 | .pipe(gulp.dest(path.DEST_SRC)) 81 | ); 82 | } 83 | 84 | gulp.task('build_html', function(){ 85 | gulp.src(path.HTML_INDEX) 86 | .pipe(hash_src( 87 | {build_dir: "./", 88 | src_path: "./dist/src/", 89 | verbose: true, 90 | hash_len: 6, 91 | regex: /(<\s*(?:script|link).*?(?:src|href)\s*=\s*(?:"|'))((?!.*\?nc).*?)((?:"|')\s*(?:>\s*<\/\s*(?:script|link)\s*>|\s*\/*>))/ig, 92 | analyze: function(match){ 93 | return { 94 | prefix: match[1], 95 | link: match[2], 96 | suffix: match[3] 97 | }; 98 | } 99 | })) 100 | .pipe(gulp.dest(path.DEST)); 101 | }); 102 | 103 | gulp.task('apply-prod-environment', function() { 104 | process.stdout.write("Setting NODE_ENV to 'production'" + "\n"); 105 | process.env.NODE_ENV = 'production'; 106 | if (process.env.NODE_ENV != 'production') { 107 | throw new Error("Failed to set NODE_ENV to production!!!!"); 108 | } else { 109 | process.stdout.write("Successfully set NODE_ENV to production" + "\n"); 110 | } 111 | }); 112 | 113 | gulp.task('production', ['apply-prod-environment', 'build_html', 'build_bundle']); 114 | 115 | gulp.task('default', ['build_bundle', 'watch']); -------------------------------------------------------------------------------- /images/gassistant_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/gassistant_512.png -------------------------------------------------------------------------------- /images/logo_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/logo_120.png -------------------------------------------------------------------------------- /images/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/logo_128.png -------------------------------------------------------------------------------- /images/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/logo_white.png -------------------------------------------------------------------------------- /images/messenger_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/messenger_512.png -------------------------------------------------------------------------------- /images/puff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /images/screenshots/analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/analysis.png -------------------------------------------------------------------------------- /images/screenshots/android_snapshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/android_snapshot_1.png -------------------------------------------------------------------------------- /images/screenshots/android_snapshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/android_snapshot_2.png -------------------------------------------------------------------------------- /images/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/dashboard.png -------------------------------------------------------------------------------- /images/screenshots/habit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/habit.png -------------------------------------------------------------------------------- /images/screenshots/habit_trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/habit_trend.png -------------------------------------------------------------------------------- /images/screenshots/snapshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/snapshots.png -------------------------------------------------------------------------------- /images/screenshots/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/images/screenshots/timeline.png -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | - kind: Event 4 | ancestor: yes 5 | properties: 6 | - name: date_start 7 | 8 | - kind: Goal 9 | ancestor: yes 10 | properties: 11 | - name: date 12 | 13 | - kind: Goal 14 | ancestor: yes 15 | properties: 16 | - name: dt_created 17 | direction: desc 18 | 19 | - kind: TrackingDay 20 | ancestor: yes 21 | properties: 22 | - name: date 23 | direction: desc 24 | 25 | - kind: Project 26 | ancestor: yes 27 | properties: 28 | - name: archived 29 | - name: starred 30 | - name: dt_created 31 | direction: desc 32 | 33 | - kind: Project 34 | ancestor: yes 35 | properties: 36 | - name: dt_created 37 | direction: desc 38 | 39 | - kind: Readable 40 | ancestor: yes 41 | properties: 42 | - name: read 43 | - name: dt_added 44 | direction: desc 45 | 46 | - kind: Task 47 | ancestor: yes 48 | properties: 49 | - name: archived 50 | - name: dt_created 51 | direction: desc 52 | 53 | - kind: Task 54 | ancestor: yes 55 | properties: 56 | - name: dt_due 57 | direction: desc 58 | 59 | - kind: Task 60 | ancestor: yes 61 | properties: 62 | - name: dt_done 63 | direction: desc 64 | 65 | - kind: Goal 66 | ancestor: yes 67 | properties: 68 | - name: dt_created 69 | 70 | - kind: HabitDay 71 | ancestor: yes 72 | properties: 73 | - name: dt_created 74 | 75 | - kind: MiniJournal 76 | ancestor: yes 77 | properties: 78 | - name: dt_created 79 | 80 | - kind: Report 81 | ancestor: yes 82 | properties: 83 | - name: dt_created 84 | direction: desc 85 | 86 | - kind: Task 87 | ancestor: yes 88 | properties: 89 | - name: dt_created 90 | 91 | - kind: Task 92 | ancestor: yes 93 | properties: 94 | - name: dt_created 95 | direction: desc 96 | 97 | - kind: Task 98 | ancestor: yes 99 | properties: 100 | - name: project 101 | - name: dt_created 102 | direction: desc 103 | 104 | - kind: Quote 105 | ancestor: yes 106 | properties: 107 | - name: readable 108 | - name: dt_added 109 | direction: desc 110 | 111 | - kind: Quote 112 | ancestor: yes 113 | properties: 114 | - name: dt_added 115 | direction: desc 116 | 117 | - kind: Readable 118 | ancestor: yes 119 | properties: 120 | - name: dt_added 121 | direction: desc 122 | 123 | - kind: Readable 124 | ancestor: yes 125 | properties: 126 | - name: favorite 127 | - name: dt_added 128 | direction: desc 129 | 130 | - kind: Readable 131 | ancestor: yes 132 | properties: 133 | - name: has_notes 134 | - name: dt_added 135 | direction: desc 136 | 137 | - kind: Readable 138 | ancestor: yes 139 | properties: 140 | - name: read 141 | - name: dt_read 142 | direction: desc 143 | 144 | - kind: Snapshot 145 | ancestor: yes 146 | properties: 147 | - name: dt_created 148 | direction: desc 149 | 150 | # AUTOGENERATED 151 | 152 | # This index.yaml is automatically updated whenever the Cloud Datastore 153 | # emulator detects that a new type of query is run. If you want to manage the 154 | # index.yaml file manually, remove the "# AUTOGENERATED" marker line above. 155 | # If you want to manage some indexes manually, move them above the marker line. 156 | 157 | -------------------------------------------------------------------------------- /local.requirements.txt: -------------------------------------------------------------------------------- 1 | # Install with pip install -r local.requirements.txt (local machine) 2 | funcsigs==1.0.2 3 | pbr==1.10.0 4 | mock==2.0.0 5 | WebTest==2.0.21 6 | pycrypto==2.6 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-dashboard", 3 | "version": "1.0.0", 4 | "description": "Jeremy Gordon", 5 | "main": "app.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "github.com/onejgordon/flow-dashboard" 9 | }, 10 | "scripts": { 11 | "preinstall": "npx npm-force-resolutions", 12 | "test": "jest", 13 | "purge": "./node_modules/.bin/react-native-clean-project" 14 | }, 15 | "keywords": [], 16 | "author": "Jeremy Gordon", 17 | "license": "MIT", 18 | "dependencies": { 19 | "alt": "^0.18.2", 20 | "alt-container": "^1.0.0", 21 | "alt-react": "0.0.1", 22 | "alt-utils": "^1.0.0", 23 | "bootstrap": "^4.1.3", 24 | "chart.js": "^2.5.0", 25 | "events": "^1.0.2", 26 | "fancy-log": "^1.3.2", 27 | "fbjs": "^0.2.1", 28 | "flux": "^2.1.1", 29 | "gapi-client": "0.0.3", 30 | "history": "^1.17.0", 31 | "jquery": "^3.3.1", 32 | "json5": "^0.5.1", 33 | "load-google-maps-api": "0.0.3", 34 | "lodash": "^4.17.10", 35 | "lodash.assign": "^4.0.0", 36 | "material-ui": "^0.18.7", 37 | "moment": "^2.22.2", 38 | "moment-timezone": "^0.4.0", 39 | "pace-js": "^1.0.2", 40 | "popper.js": "^1.14.5", 41 | "react": "^15.6.2", 42 | "react-addons-create-fragment": "^15.6.2", 43 | "react-addons-pure-render-mixin": "^15.6.2", 44 | "react-addons-update": "^15.6.2", 45 | "react-chartjs-2": "^2.7.2", 46 | "react-color": "^2.14.1", 47 | "react-dom": "^15.6.2", 48 | "react-g-analytics": "^0.2.6", 49 | "react-google-button": "^0.7.2", 50 | "react-input-autosize": "1.1.0", 51 | "react-life-timeline": "^1.0.11", 52 | "react-router": "^2.8.1", 53 | "react-select": "^1.2.1", 54 | "react-tap-event-plugin": "^2.0.1", 55 | "react-tooltip": "^3.2.2", 56 | "react-transition-group": "^1.1.3", 57 | "toastr": "^2.1.2" 58 | }, 59 | "devDependencies": { 60 | "babel": "^5.8.21", 61 | "babel-core": "^5.8.22", 62 | "babel-eslint": "^7.1.1", 63 | "babel-runtime": "^5.8.20", 64 | "babelify": "^6.1.3", 65 | "browserify": "^13.3.0", 66 | "browserify-resolutions": "^1.0.6", 67 | "browserify-shim": "^3.8.10", 68 | "eslint": "^4.18.2", 69 | "eslint-plugin-react": "^7.1.0", 70 | "gulp": "^3.8.10", 71 | "gulp-hash-src": "^0.1.6", 72 | "gulp-html-replace": "^1.4.3", 73 | "gulp-shell": "^0.5.2", 74 | "gulp-sourcemaps": "^2.6.4", 75 | "gulp-streamify": "1.0.2", 76 | "gulp-uglify": "^2.0.1", 77 | "jest-cli": "^18.1.0", 78 | "react-native-clean-project": "^1.0.4", 79 | "vinyl-buffer": "^1.0.0", 80 | "vinyl-source-stream": "^1.0.0", 81 | "watchify": "^3.11.0" 82 | }, 83 | "browser": { 84 | "bootstrap": "./node_modules/bootstrap/dist/js/bootstrap.js" 85 | }, 86 | "browserify": { 87 | "transform": [ 88 | "browserify-shim" 89 | ] 90 | }, 91 | "browserify-shim": { 92 | "bootstrap": { 93 | "depends": [ 94 | "jquery:jQuery" 95 | ] 96 | } 97 | }, 98 | "resolutions": { 99 | "natives": "1.1.3", 100 | "graceful-fs": "^4.2.10" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pytz/gae.py: -------------------------------------------------------------------------------- 1 | """ 2 | A pytz version that runs smoothly on Google App Engine. 3 | 4 | Based on http://appengine-cookbook.appspot.com/recipe/caching-pytz-helper/ 5 | but modified so that it can be imported normally with import pytz. 6 | 7 | Applied patches: 8 | 9 | - The zoneinfo dir is removed from pytz, as this module includes a ziped 10 | version of it. 11 | 12 | - pytz is monkey patched to load zoneinfos from a zipfile. 13 | 14 | - pytz is patched to not check all zoneinfo files when loaded. This is 15 | sad, I wish that was lazy, so it could be monkey patched. As it is, 16 | the zipfile patch doesn't work and it'll spend resources checking 17 | hundreds of files that we know aren't there. 18 | 19 | pytz caches loaded zoneinfos, and this module will additionally cache them 20 | in App Engines's cache to avoid unzipping constantly. The cache key 21 | includes the OLSON_VERSION so it is invalidated when pytz is updated. 22 | """ 23 | import os 24 | import logging 25 | import zipfile 26 | from cStringIO import StringIO 27 | 28 | 29 | log = logging.getLogger(__name__) 30 | zoneinfo = None 31 | zoneinfo_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'zoneinfo.zip')) 32 | 33 | def get_zoneinfo(): 34 | """Cache the opened zipfile in the module.""" 35 | global zoneinfo 36 | if zoneinfo is None: 37 | zoneinfo = zipfile.ZipFile(zoneinfo_path) 38 | 39 | return zoneinfo 40 | 41 | class TimezoneLoader(object): 42 | """A loader that that reads timezones using ZipFile.""" 43 | def __init__(self): 44 | self.available = {} 45 | 46 | def open_resource(self, name): 47 | """Opens a resource from the zoneinfo subdir for reading.""" 48 | # Import nested here so we can run setup.py without GAE. 49 | from google.appengine.api import memcache 50 | from pytz import OLSON_VERSION 51 | 52 | name_parts = name.lstrip('/').split('/') 53 | if os.path.pardir in name_parts: 54 | raise ValueError('Bad path segment: %r' % os.path.pardir) 55 | 56 | cache_key = 'pytz.zoneinfo.%s.%s' % (OLSON_VERSION, name) 57 | zonedata = memcache.get(cache_key) 58 | if zonedata is None: 59 | zonedata = get_zoneinfo().read('zoneinfo/' + '/'.join(name_parts)) 60 | memcache.add(cache_key, zonedata) 61 | log.info('Added timezone to memcache: %s' % cache_key) 62 | else: 63 | log.info('Loaded timezone from memcache: %s' % cache_key) 64 | 65 | return StringIO(zonedata) 66 | 67 | def resource_exists(self, name): 68 | """Return true if the given resource exists""" 69 | if name not in self.available: 70 | try: 71 | get_zoneinfo().getinfo('zoneinfo/' + name) 72 | self.available[name] = True 73 | except KeyError: 74 | self.available[name] = False 75 | 76 | return self.available[name] 77 | -------------------------------------------------------------------------------- /pytz/reference.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Reference tzinfo implementations from the Python docs. 3 | Used for testing against as they are only correct for the years 4 | 1987 to 2006. Do not use these for real code. 5 | ''' 6 | 7 | from datetime import tzinfo, timedelta, datetime 8 | from pytz import utc, UTC, HOUR, ZERO 9 | 10 | # A class building tzinfo objects for fixed-offset time zones. 11 | # Note that FixedOffset(0, "UTC") is a different way to build a 12 | # UTC tzinfo object. 13 | 14 | class FixedOffset(tzinfo): 15 | """Fixed offset in minutes east from UTC.""" 16 | 17 | def __init__(self, offset, name): 18 | self.__offset = timedelta(minutes = offset) 19 | self.__name = name 20 | 21 | def utcoffset(self, dt): 22 | return self.__offset 23 | 24 | def tzname(self, dt): 25 | return self.__name 26 | 27 | def dst(self, dt): 28 | return ZERO 29 | 30 | # A class capturing the platform's idea of local time. 31 | 32 | import time as _time 33 | 34 | STDOFFSET = timedelta(seconds = -_time.timezone) 35 | if _time.daylight: 36 | DSTOFFSET = timedelta(seconds = -_time.altzone) 37 | else: 38 | DSTOFFSET = STDOFFSET 39 | 40 | DSTDIFF = DSTOFFSET - STDOFFSET 41 | 42 | class LocalTimezone(tzinfo): 43 | 44 | def utcoffset(self, dt): 45 | if self._isdst(dt): 46 | return DSTOFFSET 47 | else: 48 | return STDOFFSET 49 | 50 | def dst(self, dt): 51 | if self._isdst(dt): 52 | return DSTDIFF 53 | else: 54 | return ZERO 55 | 56 | def tzname(self, dt): 57 | return _time.tzname[self._isdst(dt)] 58 | 59 | def _isdst(self, dt): 60 | tt = (dt.year, dt.month, dt.day, 61 | dt.hour, dt.minute, dt.second, 62 | dt.weekday(), 0, -1) 63 | stamp = _time.mktime(tt) 64 | tt = _time.localtime(stamp) 65 | return tt.tm_isdst > 0 66 | 67 | Local = LocalTimezone() 68 | 69 | # A complete implementation of current DST rules for major US time zones. 70 | 71 | def first_sunday_on_or_after(dt): 72 | days_to_go = 6 - dt.weekday() 73 | if days_to_go: 74 | dt += timedelta(days_to_go) 75 | return dt 76 | 77 | # In the US, DST starts at 2am (standard time) on the first Sunday in April. 78 | DSTSTART = datetime(1, 4, 1, 2) 79 | # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct. 80 | # which is the first Sunday on or after Oct 25. 81 | DSTEND = datetime(1, 10, 25, 1) 82 | 83 | class USTimeZone(tzinfo): 84 | 85 | def __init__(self, hours, reprname, stdname, dstname): 86 | self.stdoffset = timedelta(hours=hours) 87 | self.reprname = reprname 88 | self.stdname = stdname 89 | self.dstname = dstname 90 | 91 | def __repr__(self): 92 | return self.reprname 93 | 94 | def tzname(self, dt): 95 | if self.dst(dt): 96 | return self.dstname 97 | else: 98 | return self.stdname 99 | 100 | def utcoffset(self, dt): 101 | return self.stdoffset + self.dst(dt) 102 | 103 | def dst(self, dt): 104 | if dt is None or dt.tzinfo is None: 105 | # An exception may be sensible here, in one or both cases. 106 | # It depends on how you want to treat them. The default 107 | # fromutc() implementation (called by the default astimezone() 108 | # implementation) passes a datetime with dt.tzinfo is self. 109 | return ZERO 110 | assert dt.tzinfo is self 111 | 112 | # Find first Sunday in April & the last in October. 113 | start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) 114 | end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) 115 | 116 | # Can't compare naive to aware objects, so strip the timezone from 117 | # dt first. 118 | if start <= dt.replace(tzinfo=None) < end: 119 | return HOUR 120 | else: 121 | return ZERO 122 | 123 | Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") 124 | Central = USTimeZone(-6, "Central", "CST", "CDT") 125 | Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") 126 | Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") 127 | 128 | -------------------------------------------------------------------------------- /pytz/tzfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | $Id: tzfile.py,v 1.8 2004/06/03 00:15:24 zenzen Exp $ 4 | ''' 5 | 6 | from cStringIO import StringIO 7 | from datetime import datetime, timedelta 8 | from struct import unpack, calcsize 9 | 10 | from pytz.tzinfo import StaticTzInfo, DstTzInfo, memorized_ttinfo 11 | from pytz.tzinfo import memorized_datetime, memorized_timedelta 12 | 13 | 14 | def build_tzinfo(zone, fp): 15 | head_fmt = '>4s c 15x 6l' 16 | head_size = calcsize(head_fmt) 17 | (magic, format, ttisgmtcnt, ttisstdcnt,leapcnt, timecnt, 18 | typecnt, charcnt) = unpack(head_fmt, fp.read(head_size)) 19 | 20 | # Make sure it is a tzfile(5) file 21 | assert magic == 'TZif' 22 | 23 | # Read out the transition times, localtime indices and ttinfo structures. 24 | data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict( 25 | timecnt=timecnt, ttinfo='lBB'*typecnt, charcnt=charcnt) 26 | data_size = calcsize(data_fmt) 27 | data = unpack(data_fmt, fp.read(data_size)) 28 | 29 | # make sure we unpacked the right number of values 30 | assert len(data) == 2 * timecnt + 3 * typecnt + 1 31 | transitions = [memorized_datetime(trans) 32 | for trans in data[:timecnt]] 33 | lindexes = list(data[timecnt:2 * timecnt]) 34 | ttinfo_raw = data[2 * timecnt:-1] 35 | tznames_raw = data[-1] 36 | del data 37 | 38 | # Process ttinfo into separate structs 39 | ttinfo = [] 40 | tznames = {} 41 | i = 0 42 | while i < len(ttinfo_raw): 43 | # have we looked up this timezone name yet? 44 | tzname_offset = ttinfo_raw[i+2] 45 | if tzname_offset not in tznames: 46 | nul = tznames_raw.find('\0', tzname_offset) 47 | if nul < 0: 48 | nul = len(tznames_raw) 49 | tznames[tzname_offset] = tznames_raw[tzname_offset:nul] 50 | ttinfo.append((ttinfo_raw[i], 51 | bool(ttinfo_raw[i+1]), 52 | tznames[tzname_offset])) 53 | i += 3 54 | 55 | # Now build the timezone object 56 | if len(transitions) == 0: 57 | ttinfo[0][0], ttinfo[0][2] 58 | cls = type(zone, (StaticTzInfo,), dict( 59 | zone=zone, 60 | _utcoffset=memorized_timedelta(ttinfo[0][0]), 61 | _tzname=ttinfo[0][2])) 62 | else: 63 | # Early dates use the first standard time ttinfo 64 | i = 0 65 | while ttinfo[i][1]: 66 | i += 1 67 | if ttinfo[i] == ttinfo[lindexes[0]]: 68 | transitions[0] = datetime.min 69 | else: 70 | transitions.insert(0, datetime.min) 71 | lindexes.insert(0, i) 72 | 73 | # calculate transition info 74 | transition_info = [] 75 | for i in range(len(transitions)): 76 | inf = ttinfo[lindexes[i]] 77 | utcoffset = inf[0] 78 | if not inf[1]: 79 | dst = 0 80 | else: 81 | for j in range(i-1, -1, -1): 82 | prev_inf = ttinfo[lindexes[j]] 83 | if not prev_inf[1]: 84 | break 85 | dst = inf[0] - prev_inf[0] # dst offset 86 | 87 | if dst <= 0: # Bad dst? Look further. 88 | for j in range(i+1, len(transitions)): 89 | stdinf = ttinfo[lindexes[j]] 90 | if not stdinf[1]: 91 | dst = inf[0] - stdinf[0] 92 | if dst > 0: 93 | break # Found a useful std time. 94 | 95 | tzname = inf[2] 96 | 97 | # Round utcoffset and dst to the nearest minute or the 98 | # datetime library will complain. Conversions to these timezones 99 | # might be up to plus or minus 30 seconds out, but it is 100 | # the best we can do. 101 | utcoffset = int((utcoffset + 30) / 60) * 60 102 | dst = int((dst + 30) / 60) * 60 103 | transition_info.append(memorized_ttinfo(utcoffset, dst, tzname)) 104 | 105 | cls = type(zone, (DstTzInfo,), dict( 106 | zone=zone, 107 | _utc_transition_times=transitions, 108 | _transition_info=transition_info)) 109 | 110 | return cls() 111 | 112 | if __name__ == '__main__': 113 | import os.path 114 | from pprint import pprint 115 | base = os.path.join(os.path.dirname(__file__), 'zoneinfo') 116 | tz = build_tzinfo('Australia/Melbourne', 117 | open(os.path.join(base,'Australia','Melbourne'), 'rb')) 118 | tz = build_tzinfo('US/Eastern', 119 | open(os.path.join(base,'US','Eastern'), 'rb')) 120 | pprint(tz._utc_transition_times) 121 | #print tz.asPython(4) 122 | #print tz.transitions_mapping 123 | -------------------------------------------------------------------------------- /pytz/zoneinfo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/pytz/zoneinfo.zip -------------------------------------------------------------------------------- /queue.yaml: -------------------------------------------------------------------------------- 1 | queue: 2 | - name: default 3 | rate: 1/s 4 | - name: report-queue 5 | rate: 2/s 6 | - name: background-deletion-queue 7 | rate: 2/s -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Install with pip install -t lib -r requirements.txt (into lib/) 2 | rsa==4.7 3 | beautifulsoup4 4 | evernote==1.25.2 5 | oauth2client==3.0.0 6 | # google-cloud-bigquery==0.21.0 7 | google_api_python_client==1.5.1 8 | GoogleAppEngineCloudStorageClient==1.9.22.1 9 | pyjwt==1.4.1 10 | pybase62 -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: ./deploy.sh 0-1 [app.yaml] 4 | 5 | 6 | INDEX_YAML="index.yaml" 7 | CRON_YAML="cron.yaml" 8 | QUEUE_YAML="queue.yaml" 9 | 10 | check_tests(){ 11 | ./run_tests.sh 12 | RESULT=$? 13 | if [ $RESULT -ne 0 ]; then 14 | echo -e "\nUNIT TESTS FAILED!\n" 15 | cancel_deploy 16 | fi 17 | } 18 | 19 | rollback(){ 20 | echo -e "\nRolling back.....\n" 21 | python APPCFG rollback --oauth2 $(dirname $0) 22 | } 23 | 24 | build_js(){ 25 | gulp production --fatal=error --prod_deploy=$production_version 26 | RESULT=$? 27 | if [ $RESULT -ne 0 ]; then 28 | echo -e "\n GULP BUILD FAILED!\n" 29 | cancel_deploy 30 | fi 31 | } 32 | 33 | deploy(){ 34 | check_tests 35 | build_js 36 | cd ../ 37 | # Avoid deploy if config not setup 38 | if gcloud config configurations activate flow 39 | then 40 | gcloud app deploy $deploy_configs --quiet --version=$version --no-promote 41 | fi 42 | } 43 | 44 | cancel_deploy(){ 45 | echo -e "\nExitted without updating $version!\n" 46 | exit 1 47 | } 48 | 49 | sudo echo "" # cover any sudos required downstream 50 | version=$1 51 | deploy_configs="${@:2}" # Remaining args 52 | # first do a git pull to bring down tags 53 | git pull 54 | # production versions only contain digits, hf and - (dash) 55 | production_version=false 56 | # note: keep in sync with constants.PROD_VERSION_REGEX 57 | if [[ $version =~ ^[0-9\-]+[a-z]?$ ]]; then 58 | production_version=true 59 | env="production" 60 | # if deploying to production, it is compulsory to deploy all services 61 | deploy_configs="app.yaml $INDEX_YAML $CRON_YAML $QUEUE_YAML" 62 | else 63 | # cron/index/queue must be specified explicitly 64 | env="staging" 65 | fi 66 | 67 | s_length=$(echo $deploy_configs | wc -c) 68 | if [ "$s_length" -gt 1 ]; then 69 | prom="Are you sure you want to deploy to $version with configs $deploy_configs? (y/n) " 70 | else 71 | prom="Are you sure you want to deploy to $version? (y/n) " 72 | fi 73 | 74 | read -p "$prom" -n 1 -r 75 | echo 76 | 77 | if [[ $REPLY =~ ^[Yy]$ ]]; then 78 | #production versions only contain digits, hf and - (dash) 79 | if [[ $version =~ ^[0-9hf\-]+$ ]]; then 80 | read -p "This looks like a production version ($version), Are you really sure? (y/n) " -n 1 -r 81 | echo 82 | if [[ $REPLY =~ ^[Yy]$ ]]; then 83 | # if no tag yet create it, then push tags 84 | git tag -a -m "New production version by $(whoami) on $(date)" "v$version" 85 | git push --tags 86 | # deploy production version 87 | deploy 88 | echo -e "\n\nDeploy to production Successful!\n" 89 | else 90 | cancel_deploy 91 | fi 92 | else 93 | #deploy non-production version 94 | deploy 95 | fi 96 | else 97 | cancel_deploy 98 | fi 99 | -------------------------------------------------------------------------------- /scripts/fetch_gae_sdk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2015 Google Inc. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Retrieved from https://github.com/Google/oauth2client 17 | """Fetch the most recent GAE SDK and decompress it in the current directory. 18 | Usage: 19 | fetch_gae_sdk.py [] 20 | Current releases are listed here: 21 | https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured 22 | """ 23 | 24 | import json 25 | import os 26 | import StringIO 27 | import sys 28 | import urllib2 29 | import zipfile 30 | 31 | _SDK_URL = ( 32 | 'https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured') 33 | 34 | 35 | def get_gae_versions(): 36 | try: 37 | version_info_json = urllib2.urlopen(_SDK_URL).read() 38 | except: 39 | return {} 40 | try: 41 | version_info = json.loads(version_info_json) 42 | except: 43 | return {} 44 | return version_info.get('items', {}) 45 | 46 | 47 | def _version_tuple(v): 48 | version_string = os.path.splitext(v['name'])[0].rpartition('_')[2] 49 | return tuple(int(x) for x in version_string.split('.')) 50 | 51 | 52 | def get_sdk_urls(sdk_versions): 53 | python_releases = [ 54 | v for v in sdk_versions 55 | if v['name'].startswith('featured/google_appengine')] 56 | current_releases = sorted( 57 | python_releases, key=_version_tuple, reverse=True) 58 | return [release['mediaLink'] for release in current_releases] 59 | 60 | 61 | def main(argv): 62 | if len(argv) > 2: 63 | print('Usage: {} []'.format(argv[0])) 64 | return 1 65 | dest_dir = argv[1] if len(argv) > 1 else '.' 66 | if not os.path.exists(dest_dir): 67 | os.makedirs(dest_dir) 68 | 69 | if os.path.exists(os.path.join(dest_dir, 'google_appengine')): 70 | print('GAE SDK already installed at {}, exiting.'.format(dest_dir)) 71 | return 0 72 | 73 | sdk_versions = get_gae_versions() 74 | if not sdk_versions: 75 | print('Error fetching GAE SDK version info') 76 | return 1 77 | sdk_urls = get_sdk_urls(sdk_versions) 78 | for sdk_url in sdk_urls: 79 | try: 80 | sdk_contents = StringIO.StringIO(urllib2.urlopen(sdk_url).read()) 81 | break 82 | except: 83 | pass 84 | else: 85 | print('Could not read SDK from any of {}'.format(sdk_urls)) 86 | return 1 87 | sdk_contents.seek(0) 88 | try: 89 | zip_contents = zipfile.ZipFile(sdk_contents) 90 | zip_contents.extractall(dest_dir) 91 | print('GAE SDK Installed to {}.'.format(dest_dir)) 92 | except: 93 | print('Error extracting SDK contents') 94 | return 1 95 | 96 | if __name__ == '__main__': 97 | sys.exit(main(sys.argv[:])) -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | module=${1:-all} 3 | sudo python runtests.py ~/google-cloud-sdk/platform/google_appengine ../testing/ $module -------------------------------------------------------------------------------- /scripts/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import optparse 3 | import sys 4 | import unittest 5 | import doctest 6 | from os.path import dirname, abspath 7 | 8 | USAGE = """%prog SDK_PATH TEST_PATH 9 | Run unit tests for App Engine apps. 10 | 11 | SDK_PATH Path to the SDK installation 12 | TEST_PATH Path to package containing test modules 13 | MODULE Name of test module to run (optional, if not provided, run all) 14 | """ 15 | 16 | 17 | def main(sdk_path, test_path, module=None): 18 | p = dirname(abspath(test_path)) 19 | sys.path.append(p) 20 | sys.path.insert(0, sdk_path) 21 | sys.path.insert(0, 'lib') 22 | 23 | import dev_appserver 24 | 25 | dev_appserver.fix_sys_path() 26 | if module and module != 'all': 27 | suite = unittest.loader.TestLoader().discover(test_path, pattern=module) 28 | else: 29 | suite = unittest.loader.TestLoader().discover(test_path) 30 | doctest_modules = ["tools"] 31 | for mod in doctest_modules: 32 | suite.addTests(doctest.DocTestSuite(mod)) 33 | test_result = unittest.TextTestRunner(verbosity=2).run(suite) 34 | sys.exit(0 if test_result.wasSuccessful() else 1) 35 | 36 | 37 | if __name__ == '__main__': 38 | parser = optparse.OptionParser(USAGE) 39 | options, args = parser.parse_args() 40 | if len(args) < 2: 41 | print 'Error: 2+ arguments required.' 42 | parser.print_help() 43 | sys.exit(1) 44 | SDK_PATH = args[0] 45 | TEST_PATH = args[1] 46 | MODULE = args[2] if len(args) > 2 else None 47 | main(SDK_PATH, TEST_PATH, module=MODULE) 48 | -------------------------------------------------------------------------------- /scripts/server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ../ 4 | if [ "$1" == "kill" ]; then 5 | echo "Killing server..." 6 | lsof -P | grep ':8080' | awk '{print $2}' | xargs kill -9 7 | elif [ "$1" == "clean" ]; then 8 | echo "Starting server (cleaning db)..." 9 | dev_appserver.py app.yaml --log_level=debug --clear_datastore=yes --enable_host_checking 10 | else 11 | echo "Starting server..." 12 | dev_appserver.py app.yaml --log_level=debug --enable_host_checking --enable_console 13 | fi -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/services/__init__.py -------------------------------------------------------------------------------- /services/flow_evernote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # API calls to interact with Evernote 5 | # https://github.com/evernote/evernote-sdk-python 6 | 7 | import logging 8 | from datetime import datetime 9 | from evernote.api.client import EvernoteClient 10 | from evernote.edam.error.ttypes import EDAMSystemException 11 | import re 12 | import tools 13 | from google.appengine.api import memcache 14 | 15 | SANDBOX = False 16 | USE_DEV_TOKEN = False 17 | SECRET_MCK = "user:%s:evernote:secret" 18 | 19 | 20 | def user_access_token(user): 21 | from settings.secrets import EVERNOTE_DEV_TOKEN 22 | if USE_DEV_TOKEN: 23 | access_token = EVERNOTE_DEV_TOKEN 24 | else: 25 | access_token = user.get_integration_prop('evernote_access_token') 26 | return access_token 27 | 28 | 29 | def get_request_token(user, callback): 30 | ''' 31 | Get request token 32 | ''' 33 | from settings import secrets 34 | client = EvernoteClient( 35 | consumer_key=secrets.EVERNOTE_CONSUMER_KEY, 36 | consumer_secret=secrets.EVERNOTE_CONSUMER_SECRET, 37 | sandbox=SANDBOX 38 | ) 39 | request_token = client.get_request_token(callback) 40 | logging.debug(request_token) 41 | # Save secret 42 | memcache.set(SECRET_MCK % user.key.id(), request_token['oauth_token_secret']) 43 | authorize_url = client.get_authorize_url(request_token) 44 | return authorize_url 45 | 46 | 47 | def get_access_token(user, oauth_token, oauth_verifier): 48 | ''' 49 | Get request token 50 | ''' 51 | from settings import secrets 52 | client = EvernoteClient( 53 | consumer_key=secrets.EVERNOTE_CONSUMER_KEY, 54 | consumer_secret=secrets.EVERNOTE_CONSUMER_SECRET, 55 | sandbox=SANDBOX 56 | ) 57 | access_token = en_user_id = None 58 | access_token_dict = {} 59 | oauth_token_secret = memcache.get(SECRET_MCK % user.key.id()) 60 | if oauth_token_secret: 61 | try: 62 | access_token_dict = client.get_access_token_dict( 63 | oauth_token, 64 | oauth_token_secret, 65 | oauth_verifier 66 | ) 67 | except KeyError: 68 | logging.warning("KeyError getting Evernote access token") 69 | en_user_id = access_token_dict.get('edam_userId') 70 | access_token = access_token_dict.get('oauth_token') 71 | else: 72 | logging.warning("oauth_token_secret unavailable") 73 | return (access_token, en_user_id) 74 | 75 | 76 | def extract_clipping_content(raw): 77 | m = re.search(r'(.*)<\/en-note>', raw) 78 | if m: 79 | content = m.groups()[0] 80 | if content: 81 | return tools.remove_html_tags(content) 82 | 83 | 84 | def get_note(user, note_id): 85 | STRIP_WORDS = ["Pocket:"] 86 | title = url = content = None 87 | access_token = user_access_token(user) 88 | if access_token: 89 | client = EvernoteClient(token=access_token, sandbox=SANDBOX) 90 | noteStore = client.get_note_store() 91 | note = noteStore.getNote(access_token, note_id, True, False, False, False) 92 | if note: 93 | logging.debug(note) 94 | content = extract_clipping_content(note.content) 95 | title = note.title 96 | uid = note.guid 97 | for sw in STRIP_WORDS: 98 | if sw in title: 99 | title = title.replace(sw, '') 100 | title = title.strip() 101 | attrs = note.attributes 102 | if attrs: 103 | url = attrs.sourceURL 104 | else: 105 | logging.debug("Note not found") 106 | else: 107 | logging.warning("Access token not available") 108 | return (uid, title, content, url) 109 | 110 | if __name__ == "__main__": 111 | pass -------------------------------------------------------------------------------- /services/github.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # API calls to interact with Github 5 | 6 | from google.appengine.api import urlfetch 7 | import base64 8 | import json 9 | import logging 10 | import urllib 11 | from datetime import datetime, timedelta, time 12 | from google.appengine.api import memcache 13 | import tools 14 | from bs4 import BeautifulSoup 15 | 16 | BASE = 'https://api.github.com' 17 | REPO_MEMKEY = "GITHUB:%s" 18 | GH_DATE = "%Y-%m-%dT%H:%M:%SZ" 19 | 20 | 21 | class GithubClient(object): 22 | 23 | def __init__(self, user): 24 | self.user = user 25 | self.pat = self.user.get_integration_prop('github_pat') 26 | self.github_username = self.user.get_integration_prop('github_username') 27 | 28 | def _can_run(self): 29 | return self.pat and self.github_username 30 | 31 | def _parse_raw_date(self, date): 32 | return datetime.strptime(date, GH_DATE) 33 | 34 | def api_call(self, url): 35 | ''' 36 | Return tuple (response_object, json parsed response) 37 | ''' 38 | if not url.startswith('http'): 39 | url = BASE + url 40 | auth_header = {"Authorization": "Basic %s" % base64.b64encode("%s:%s" % (self.github_username, self.pat))} 41 | logging.debug("GET %s" % url) 42 | response = urlfetch.fetch(url, method="GET", deadline=60, headers=auth_header) 43 | if response.status_code == 200: 44 | return (response, json.loads(response.content)) 45 | else: 46 | logging.debug(response.content) 47 | return (response, None) 48 | 49 | def get_contributions_on_date_range(self, date_range): 50 | ''' 51 | Currently scraping Github public overview page (no API yet) 52 | ''' 53 | response = urlfetch.fetch("https://github.com/%s?tab=overview" % self.github_username, deadline=30) 54 | if response.status_code == 200: 55 | bs = BeautifulSoup(response.content, "html.parser") 56 | commits_dict = {} 57 | for date in date_range: 58 | iso_date = tools.iso_date(date) 59 | commits_on_day = bs.find('rect', {'data-date': iso_date}).get('data-count', 0) 60 | commits_dict[date] = commits_on_day 61 | return commits_dict 62 | else: 63 | logging.error("Error getting contributions") 64 | -------------------------------------------------------------------------------- /services/goodreads.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from settings.secrets import GR_API_KEY 3 | from google.appengine.api import urlfetch 4 | from google.appengine.ext import ndb 5 | from lxml import etree 6 | from StringIO import StringIO 7 | from models import Readable 8 | from constants import READABLE 9 | import urllib 10 | 11 | 12 | def get_books_on_shelf(user, shelf='currently-reading'): 13 | ''' 14 | Return JSON array {title, author, isbn, image} 15 | ''' 16 | user_id = user.get_integration_prop('goodreads_user_id') 17 | readables = [] 18 | success = False 19 | if user_id: 20 | data = urllib.urlencode({ 21 | 'shelf': shelf, 22 | 'key': GR_API_KEY, 23 | 'v': 2 24 | }) 25 | params = data 26 | url = "https://www.goodreads.com/review/list/%s.xml?%s" % (user_id, params) 27 | logging.debug("Fetching %s for %s" % (url, user)) 28 | res = urlfetch.fetch( 29 | url=url, 30 | method=urlfetch.GET, 31 | validate_certificate=True) 32 | logging.debug(res.status_code) 33 | if res.status_code == 200: 34 | xml = res.content 35 | data = etree.parse(StringIO(xml)) 36 | for r in data.getroot().find('reviews').findall('review'): 37 | book = r.find('book') 38 | isbn = book.find('isbn13').text 39 | image_url = book.find('image_url').text 40 | title = book.find('title').text 41 | authors = book.find('authors') 42 | link = book.find('link').text 43 | first_author = authors.find('author') 44 | if first_author is not None: 45 | name = first_author.find('name') 46 | if name is not None: 47 | author = name.text 48 | r = Readable.CreateOrUpdate(user, isbn, title=title, 49 | url=link, source='goodreads', 50 | image_url=image_url, author=author, 51 | type=READABLE.BOOK, 52 | read=False) 53 | readables.append(r) 54 | success = True 55 | logging.debug("Putting %d readable(s)" % len(readables)) 56 | ndb.put_multi(readables) 57 | Readable.put_sd_batch(readables) 58 | return (success, readables) 59 | 60 | -------------------------------------------------------------------------------- /services/gservice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from apiclient import discovery 5 | import logging 6 | from oauth2client import client 7 | import httplib2 8 | from datetime import datetime, timedelta 9 | import tools 10 | from constants import SECURE_BASE 11 | from settings.secrets import GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET 12 | from oauth2client.client import GoogleCredentials 13 | 14 | 15 | class GoogleServiceFetcher(object): 16 | 17 | def __init__(self, user, api='fitness', version='v3', 18 | scopes=None, credential_type='user'): 19 | self.user = user 20 | self.service = None 21 | self.api = api 22 | self.version = version 23 | self.credentials = None 24 | self.http_auth = None 25 | self.credential_type = credential_type 26 | if credential_type == 'user': 27 | self.get_user_credentials_object() 28 | else: 29 | self.get_application_credentials_object() 30 | self.scopes = scopes if scopes else [] 31 | 32 | def build_service(self): 33 | ok = False 34 | logging.debug("Building %s service for %s (%s)" % (self.credential_type, self.api, self.version)) 35 | kwargs = {} 36 | if self.credential_type == 'user': 37 | if not self.http_auth: 38 | self.get_http_auth() 39 | kwargs['http'] = self.http_auth 40 | ok = bool(self.http_auth) 41 | else: 42 | kwargs['credentials'] = self.credentials 43 | ok = bool(self.credentials) 44 | self.service = discovery.build(self.api, self.version, **kwargs) 45 | if not ok: 46 | logging.warning("Failed to build service for %s (%s) - Credential failure?" % (self.api, self.version)) 47 | return ok 48 | 49 | def set_google_credentials(self, credentials_object): 50 | logging.debug(credentials_object.to_json()) 51 | self.user.set_integration_prop('google_credentials', credentials_object.to_json()) 52 | self.user.put() 53 | 54 | def get_google_credentials(self): 55 | return self.user.get_integration_prop('google_credentials', {}) 56 | 57 | def get_auth_flow(self, scope): 58 | base = 'http://localhost:8080' if tools.on_dev_server() else SECURE_BASE 59 | flow = client.OAuth2WebServerFlow(client_id=GOOGLE_CLIENT_ID, 60 | client_secret=GOOGLE_CLIENT_SECRET, 61 | scope=scope, 62 | access_type='offline', 63 | prompt='consent', 64 | redirect_uri=base + "/api/auth/google/oauth2callback") 65 | flow.params['include_granted_scopes'] = 'true' 66 | # flow.params['access_type'] = 'offline' 67 | return flow 68 | 69 | def get_user_credentials_object(self): 70 | if not self.credentials: 71 | cr_json = self.get_google_credentials() 72 | if cr_json: 73 | # Note JSON is stored as escaped string, not dict 74 | cr = client.Credentials.new_from_json(cr_json) 75 | expires_in = cr.token_expiry - datetime.utcnow() 76 | logging.debug("expires_in: %s" % expires_in) 77 | if expires_in < timedelta(minutes=15): 78 | try: 79 | cr.refresh(httplib2.Http()) 80 | except client.HttpAccessTokenRefreshError, e: 81 | logging.error("HttpAccessTokenRefreshError: %s" % e) 82 | cr = None 83 | else: 84 | self.set_google_credentials(cr) 85 | self.credentials = cr 86 | return cr 87 | 88 | def get_application_credentials_object(self): 89 | if not self.credentials: 90 | self.credentials = GoogleCredentials.get_application_default() 91 | 92 | def get_auth_uri(self, state=None): 93 | flow = self.get_auth_flow(scope=' '.join(self.scopes)) 94 | auth_uri = flow.step1_get_authorize_url(state=state) 95 | return auth_uri 96 | 97 | def get_http_auth(self): 98 | self.get_user_credentials_object() 99 | if self.credentials: 100 | self.http_auth = self.credentials.authorize(httplib2.Http()) 101 | 102 | def check_available_scopes(self): 103 | scopes = self.credentials.retrieve_scopes(httplib2.Http()) 104 | missing_scopes = [] 105 | if self.scopes: 106 | for scope in self.scopes: 107 | if scope not in scopes: 108 | missing_scopes.append(scope) 109 | if missing_scopes: 110 | logging.debug("Missing scopes: %s" % missing_scopes) 111 | return missing_scopes 112 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/settings/__init__.py -------------------------------------------------------------------------------- /settings/secrets_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | # To generate, run in Python: 5 | # import os 6 | # os.urandom(48) 7 | COOKIE_KEY = '' 8 | 9 | # GCP project info 10 | GOOGLE_PROJECT_ID = "flow-app-xxxx" 11 | GOOGLE_PROJECT_NO = 0 12 | 13 | # Create an oauth 2.0 web client ID from GCP console 14 | # Configure our client ID with, authorized javascript origins: 15 | # - https://[your-project-id].appspot.com 16 | # - https://test-dot-[your-project-id].appspot.com (optional, to enable testing on a subversion) 17 | # And authorised redirect URIs: 18 | # - https://[your-project-id].appspot.com/api/auth/google/oauth2callback 19 | GOOGLE_CLIENT_ID = "######.XXXXXXXXXXXX.apps.googleusercontent.com" 20 | GOOGLE_CLIENT_SECRET = "XXXXXXXXX" 21 | 22 | # Create a second oauth 2.0 client ID for use on the dev server. Origins: 23 | # - http://localhost:8080 24 | # Redirects: 25 | # - http://localhost:8080/api/auth/google/oauth2callback 26 | DEV_GOOGLE_CLIENT_ID = "######.XXXXXXXXXXXX.apps.googleusercontent.com" 27 | 28 | # Create a new API key from GCP console (optional) 29 | G_MAPS_API_KEY = "XXXXXXXX" 30 | 31 | # AES Cypher Key (generate similarly to above with os.urandom(16)) 32 | AES_CYPHER_KEY = '16 byte key ....' 33 | 34 | # Good Reads (optional) 35 | GR_API_KEY = "" 36 | GR_SECRET = "" 37 | 38 | # Pocket (optional) 39 | POCKET_CONSUMER_KEY = "" 40 | 41 | # Evernote (optional) 42 | EVERNOTE_CONSUMER_KEY = "" 43 | EVERNOTE_CONSUMER_SECRET = "" 44 | EVERNOTE_DEV_TOKEN = "" 45 | 46 | # Facebook (optional) 47 | FB_ACCESS_TOKEN = "" 48 | FB_VERIFY_TOKEN = "" 49 | 50 | # Dialogflow (Previously API.AI, optional) 51 | API_AI_AUTH_KEY = "" 52 | API_AI_FB_CALLBACK = "" 53 | -------------------------------------------------------------------------------- /src/error.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | {{ SITENAME }} | Error 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | {{ SITENAME }} 16 |
17 |
18 | 19 | 20 |
21 |
22 |

Sorry, an error has occurred

23 | 24 |

Rest assured, we're working on the problem!

25 | 26 | {% if traceback %} 27 |

{{ traceback|escape }}

28 | {% endif %} 29 |
30 |
31 | 32 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ SITENAME }} | {{ TAGLINE }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/js/__tests__/util-test.js: -------------------------------------------------------------------------------- 1 | 2 | jest.dontMock('../utils/util'); 3 | 4 | var util = require('../utils/util'); 5 | 6 | describe('util.type_check', function() { 7 | it('converts a number type of "2.5" to the float 2.5', function() { 8 | expect(util.type_check("2.5", "number")).toBe(2.5); 9 | }); 10 | }); 11 | 12 | describe('util.toggleInList', function() { 13 | it('removes "dog" from a short array', function() { 14 | var res = util.toggleInList(["dog","cat"], "dog"); 15 | expect(res[0]).toBe("cat"); 16 | expect(res.length).toBe(1); 17 | }); 18 | }); 19 | 20 | describe('util.transp_color', function() { 21 | it('adds transparency to a hex color string (with hash)', function() { 22 | var res = util.transp_color('#CCCCCC', 0.5); 23 | expect(res).toBe("#7FCCCCCC"); 24 | }); 25 | it('adds transparency to a hex color string (without hash)', function() { 26 | var res = util.transp_color('CCCCCC', 0.5); 27 | expect(res).toBe("#7FCCCCCC"); 28 | }); 29 | }); -------------------------------------------------------------------------------- /src/js/actions/ProjectActions.js: -------------------------------------------------------------------------------- 1 | 2 | import alt from 'config/alt.js'; 3 | 4 | 5 | class ProjectActions { 6 | constructor() { 7 | this.generateActions('fetchingProjects', 'fetchingProjectsFailed', 'updatingProject', 'updatingProjectFailed'); 8 | } 9 | 10 | gotProjects(result) { 11 | return { 12 | projects: result.projects 13 | } 14 | } 15 | 16 | updatedProject(result) { 17 | return result.project 18 | } 19 | } 20 | 21 | export default alt.createActions(ProjectActions); 22 | -------------------------------------------------------------------------------- /src/js/actions/TaskActions.js: -------------------------------------------------------------------------------- 1 | 2 | import alt from 'config/alt.js'; 3 | 4 | 5 | class TaskActions { 6 | constructor() { 7 | this.generateActions('openTaskDialog', 'closeTaskDialog'); 8 | } 9 | 10 | } 11 | 12 | export default alt.createActions(TaskActions); 13 | -------------------------------------------------------------------------------- /src/js/actions/UserActions.js: -------------------------------------------------------------------------------- 1 | var alt = require('config/alt'); 2 | var api = require('utils/api'); 3 | 4 | class UserActions { 5 | 6 | constructor() { 7 | // Automatic action 8 | this.generateActions('loadLocalUser', 'storeUser'); 9 | } 10 | 11 | // Manual actions 12 | 13 | logout() { 14 | return function(dispatch) { 15 | try { 16 | api.get("/api/auth/logout", {}, (res) => { 17 | dispatch({ success: res.success }); 18 | }); 19 | } catch (err) { 20 | console.error(err); 21 | dispatch({ok: false, error: err.data}); 22 | } 23 | } 24 | } 25 | 26 | update(data) { 27 | return (dispatch) => { 28 | api.post("/api/user/me", data, (res) => { 29 | dispatch(res); 30 | }) 31 | } 32 | } 33 | } 34 | 35 | module.exports = alt.createActions(UserActions); -------------------------------------------------------------------------------- /src/js/components/Analysis.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var util = require('utils/util'); 4 | import {IconButton, FlatButton} from 'material-ui'; 5 | var api = require('utils/api'); 6 | import {get} from 'lodash'; 7 | import {Link} from 'react-router'; 8 | import connectToStores from 'alt-utils/lib/connectToStores'; 9 | var UserStore = require('stores/UserStore'); 10 | 11 | @connectToStores 12 | export default class Analysis extends React.Component { 13 | static defaultProps = {}; 14 | constructor(props) { 15 | super(props); 16 | let today = new Date(); 17 | let start = new Date(); 18 | let end = new Date(); 19 | this.INITIAL_RANGE = 14; 20 | let user = props.user; 21 | let questions = []; 22 | if (user) questions = get(user, 'settings.journals.questions', []); 23 | let chart_enabled = questions.filter((q) => { 24 | return q.chart_default; 25 | }).map((q) => {return q.name;}); 26 | start.setDate(today.getDate() - this.INITIAL_RANGE); 27 | end.setDate(today.getDate() + 1); // Include all of today 28 | this.state = { 29 | start: start, 30 | end: end, 31 | iso_dates: [], 32 | journals: [], 33 | goals: {}, 34 | habits: [], 35 | tasks: [], 36 | productivity: [], 37 | habitdays: {}, 38 | tags: [], 39 | loaded: false, 40 | tags_loading: false, 41 | questions: questions, 42 | chart_enabled_questions: chart_enabled 43 | }; 44 | 45 | this.handle_update = this.handle_update.bind(this) 46 | } 47 | 48 | static getStores() { 49 | return []; 50 | } 51 | 52 | static getPropsFromStores() { 53 | return {}; 54 | } 55 | 56 | componentDidMount() { 57 | util.set_title("Analysis"); 58 | this.fetch_data(); 59 | } 60 | 61 | fetch_data() { 62 | let {start, end} = this.state; 63 | let params = { 64 | date_start: util.printDateObj(start, 'UTC'), 65 | date_end: util.printDateObj(end, 'UTC'), 66 | with_tracking: 1, 67 | with_goals: 1, 68 | with_tasks: 1 69 | } 70 | api.get("/api/analysis", params, (res) => { 71 | this.setState({ 72 | journals: res.journals, 73 | iso_dates: res.dates, 74 | tasks: res.tasks, 75 | tracking_days: res.tracking_days, 76 | goals: util.lookupDict(res.goals, 'id'), 77 | loaded: true 78 | }); 79 | }); 80 | } 81 | 82 | handle_update(key, data) { 83 | let state = this.state 84 | state[key] = data 85 | this.setState(state) 86 | } 87 | 88 | render() { 89 | let {loaded, goals, 90 | habits, habitdays, iso_dates, 91 | tracking_days, 92 | journals, tasks, end} = this.state; 93 | let today = new Date(); 94 | let admin = UserStore.admin(); 95 | if (!loaded) return null; 96 | return ( 97 |
98 | 99 |

Analysis

100 | 101 | refresh 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
Note that on all charts clicking on series labels will toggle visibility
111 | 112 | { React.cloneElement(this.props.children, { 113 | user: this.props.user, 114 | goals: goals, 115 | journals: journals, 116 | tasks: tasks, 117 | tracking_days: tracking_days, 118 | habits: habits, 119 | habitdays: habitdays, 120 | iso_dates: iso_dates, 121 | end_date: end, 122 | loaded: loaded, 123 | onUpdateData: this.handle_update }) } 124 | 125 |
126 | ); 127 | } 128 | }; 129 | 130 | module.exports = Analysis; -------------------------------------------------------------------------------- /src/js/components/Auth.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | import {Link} from 'react-router'; 3 | var AppConstants = require('constants/AppConstants'); 4 | var api = require('utils/api'); 5 | import GoogleLoginCompat from 'components/common/GoogleLoginCompat'; 6 | var client_secrets = require('constants/client_secrets'); 7 | import {RaisedButton} from 'material-ui'; 8 | var toastr = require('toastr'); 9 | 10 | export default class Auth extends React.Component { 11 | static defaultProps = {} 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | } 20 | 21 | get_provider() { 22 | let id = this.props.params.provider; 23 | return { 24 | google: { 25 | uri: '/api/auth/google_auth', 26 | params: ['client_id', 'redirect_uri', 'state', 'response_type'], 27 | name: "Google Assistant" 28 | }, 29 | fbook: { 30 | uri: '/api/auth/fbook_auth', 31 | params: ['redirect_uri', 'account_linking_token'], 32 | name: "Facebook Messenger" 33 | } 34 | }[id] 35 | } 36 | 37 | finish_auth(id_token) { 38 | let provider = this.get_provider(); 39 | if (provider) { 40 | let data = {}; 41 | provider.params.forEach((p) => { 42 | data[p] = this.props.location.query[p]; 43 | }); 44 | if (id_token) data.id_token = id_token; 45 | api.post(provider.uri, data, (res) => { 46 | if (res.redirect) window.location.replace(res.redirect); 47 | else if (res.error) toastr.error(res.error); 48 | }); 49 | } else { 50 | toastr.error("Provider not found"); 51 | } 52 | } 53 | 54 | success(gUser) { 55 | var id_token = gUser.getAuthResponse().id_token; 56 | this.finish_auth(id_token); 57 | } 58 | 59 | fail(res) { 60 | console.log(res) 61 | } 62 | 63 | render() { 64 | let SITENAME = AppConstants.SITENAME; 65 | let provider = this.get_provider() 66 | return ( 67 |
68 | 69 |
70 | 71 |

To connect to {provider.name}, sign in to {SITENAME}

72 | 73 | 74 | 75 |

Or

76 | 77 |
78 | 84 |
85 | 86 |
87 | 88 |
89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/js/components/Feedback.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var UserStore = require('stores/UserStore'); 4 | import {TextField, RaisedButton, FlatButton} from 'material-ui'; 5 | var api = require('utils/api'); 6 | var util = require('utils/util'); 7 | import connectToStores from 'alt-utils/lib/connectToStores'; 8 | import {changeHandler} from 'utils/component-utils'; 9 | 10 | @connectToStores 11 | @changeHandler 12 | export default class Feedback extends React.Component { 13 | static defaultProps = {}; 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | form: { 18 | } 19 | }; 20 | } 21 | 22 | static getStores() { 23 | return [UserStore]; 24 | } 25 | 26 | static getPropsFromStores() { 27 | return UserStore.getState(); 28 | } 29 | 30 | componentDidMount() { 31 | util.set_title("Feedback"); 32 | } 33 | 34 | submit() { 35 | let {form} = this.state; 36 | let {user} = this.props; 37 | if (user && form.feedback) { 38 | var data = { 39 | feedback: form.feedback, 40 | email: user.email 41 | } 42 | api.post("/api/feedback", data, (res) => { 43 | this.setState({form: {}}); 44 | }) 45 | } 46 | } 47 | 48 | render() { 49 | let {form} = this.state; 50 | let {user} = this.props 51 | return ( 52 |
53 | 54 |

Feedback

55 | 56 | 61 | 62 |

Your message will be sent as { user.email }.

63 | 64 |
65 | 66 | 67 |
68 |
69 |

Or, want to file a bug report?

70 | 71 |

Please create an issue on Github.

72 | 73 |
74 | 75 |
76 |
77 | ); 78 | } 79 | }; 80 | 81 | module.exports = Feedback; 82 | -------------------------------------------------------------------------------- /src/js/components/HabitHistory.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var util = require('utils/util'); 3 | import {changeHandler} from 'utils/component-utils'; 4 | var api = require('utils/api'); 5 | var FetchedList = require('components/common/FetchedList'); 6 | import {ListItem, IconButton, IconMenu, MenuItem, FontIcon} from 'material-ui' 7 | 8 | @changeHandler 9 | export default class HabitHistory extends React.Component { 10 | static defaultProps = {}; 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | componentDidMount() { 16 | util.set_title("Habit History"); 17 | } 18 | 19 | componentDidUpdate(prevProps, prevState) { 20 | } 21 | 22 | toggle_archived(h) { 23 | api.post("/api/habit", {id: h.id, archived: h.archived ? 0 : 1}, (res) => { 24 | this.refs.habits.update_item_by_key(res.habit, 'id') 25 | }) 26 | } 27 | 28 | confirm_delete(h) { 29 | var r = confirm('This will delete this habit, and all tracked history. This cannot be undone. Are you sure?'); 30 | if (r) this.delete(h) 31 | } 32 | 33 | delete(h) { 34 | api.post("/api/habit/delete", {id: h.id}, (res) => { 35 | if (res.success) this.refs.habits.remove_item_by_key(h.id, 'id') 36 | }) 37 | } 38 | 39 | render_habit(h) { 40 | let archive_icon = !h.archived ? 'archive' : 'unarchive' 41 | let menu = [ 42 | {icon: archive_icon, click: this.toggle_archived.bind(this, h), label: util.capitalize(archive_icon)} 43 | ] 44 | if (h.archived) { 45 | // Add delete option 46 | menu.push({icon: 'delete', click: this.confirm_delete.bind(this, h), label: "Delete habit"}) 47 | } 48 | let rightIcon = ( 49 | more_vert}> 50 | { menu.map((mi, i) => { 51 | return {mi.icon}} onTouchTap={mi.click}>{mi.label} 52 | }) } 53 | 54 | ) 55 | let secondary = ["Created " + util.printDate(h.ts_created)] 56 | if (h.archived) secondary.push("Archived") 57 | let st = {} 58 | if (h.archived) st.color = "#555"; 59 | let title = { h.name } 60 | return 64 | } 65 | 66 | render() { 67 | let params = {} 68 | return ( 69 |
70 | 71 |

Habit History

72 | 73 | 82 | 83 |
84 | ); 85 | } 86 | } 87 | 88 | module.exports = HabitHistory; 89 | -------------------------------------------------------------------------------- /src/js/components/NotFound.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | export default class NotFound extends React.Component{ 4 | render() { 5 | return ( 6 |
7 |

Not Found

8 |
9 | ); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/js/components/Privacy.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var util = require('utils/util'); 3 | 4 | export default class Privacy extends React.Component { 5 | static defaultProps = {} 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount() { 11 | util.set_title("Privacy Policy"); 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 |

Flow Dashboard Privacy Policy

19 | 20 |

What information does Flow collect?

21 | 22 |

The Flow Dashboard application collects explicitly volunteered user data in order to help users track goals, habits, daily tasks, and events. Flow Dashboard uses Google Cloud Platform to host all data, and retains a log of HTTP request activity for up to 60 days. Conversations hosted by Actions on the Google API are not specifically recorded, though some actions cause updates to user data in the Flow Dashboard app. To authenticate sign in, Flow Dashboard stores the email address of all users.

23 | 24 |

How does Flow use the information?

25 | 26 |

All information is collected for the purpose of providing the Flow Dashboard app services to users. Data stored for each user is owned by that user. Data can be fully cleared by request at any time, and exports can also be made available. Email addresses will never be used for anything other than opt-in notifications.

27 | 28 |

What information does Flow share?

29 | 30 |

No information is shared with third parties.

31 | 32 |

Contact

33 | 34 |

35 | onejgordon@gmail.com
36 | Web: https://flowdash.co 37 |

38 | 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/js/components/ProjectHistory.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var util = require('utils/util'); 3 | import {changeHandler} from 'utils/component-utils'; 4 | var api = require('utils/api'); 5 | var FetchedList = require('components/common/FetchedList'); 6 | import {ListItem, IconButton} from 'material-ui' 7 | 8 | @changeHandler 9 | export default class ProjectHistory extends React.Component { 10 | static defaultProps = {}; 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | componentDidMount() { 16 | util.set_title("Project History"); 17 | } 18 | 19 | componentDidUpdate(prevProps, prevState) { 20 | } 21 | 22 | toggle_archived(prj) { 23 | api.post("/api/project", {id: prj.id, archived: prj.archived ? 0 : 1}, (res) => { 24 | this.refs.projects.update_item_by_key(res.project, 'id') 25 | }) 26 | } 27 | 28 | render_project(prj) { 29 | let icon = !prj.archived ? 'archive' : 'unarchive' 30 | let rightIcon = { icon } 33 | let secondary = ["Created " + util.printDate(prj.ts_created)] 34 | if (prj.archived) secondary.push("Archived") 35 | let st = {} 36 | let title = { prj.title } 37 | return 41 | } 42 | 43 | render() { 44 | let params = {} 45 | return ( 46 |
47 | 48 |

Project History

49 | 50 | 59 | 60 |
61 | ); 62 | } 63 | } 64 | 65 | module.exports = ProjectHistory; 66 | -------------------------------------------------------------------------------- /src/js/components/Site.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PropTypes = require('prop-types'); 4 | 5 | var React = require('react'); 6 | var GoogleAnalytics = require('react-g-analytics'); 7 | var alt = require('config/alt'); 8 | var UserActions = require('actions/UserActions'); 9 | var AppConstants = require('constants/AppConstants'); 10 | import { supplyFluxContext } from 'alt-react' 11 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 12 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 13 | import {fade} from 'material-ui/utils/colorManipulator'; 14 | var toastr = require('toastr'); 15 | var pace = require('pace-js'); 16 | 17 | import { 18 | amber500, cyan700, amber400, amber700, 19 | grey600, fullWhite, white 20 | } from 'material-ui/styles/colors'; 21 | 22 | const muiTheme = getMuiTheme({ 23 | fontFamily: 'Roboto, sans-serif', 24 | palette: { 25 | primary1Color: "#45D8FF", 26 | primary2Color: cyan700, 27 | primary3Color: grey600, 28 | accent1Color: amber700, 29 | accent2Color: amber500, 30 | accent3Color: amber400, 31 | textColor: fullWhite, 32 | secondaryTextColor: fade(fullWhite, 0.7), 33 | alternateTextColor: '#303030', 34 | canvasColor: '#303030', 35 | borderColor: fade(fullWhite, 0.3), 36 | disabledColor: fade(fullWhite, 0.3), 37 | pickerHeaderColor: fade(fullWhite, 0.12), 38 | clockCircleColor: fade(fullWhite, 0.12), 39 | }, 40 | appBar: { 41 | color: '#303030', 42 | textColor: white 43 | }, 44 | raisedButton: { 45 | textColor: white, 46 | primaryTextColor: white, 47 | secondaryTextColor: white 48 | } 49 | }); 50 | 51 | class Site extends React.Component { 52 | constructor(props) { 53 | super(props); 54 | UserActions.loadLocalUser(); 55 | } 56 | 57 | componentWillMount() { 58 | pace.start({ 59 | restartOnRequestAfter: 10 // ms 60 | }); 61 | toastr.options.positionClass = "toast-bottom-left"; 62 | toastr.options.preventDuplicates = true; 63 | } 64 | 65 | componentDidMount() { 66 | } 67 | 68 | render() { 69 | var YEAR = new Date().getFullYear(); 70 | var copyright_years = AppConstants.YEAR; 71 | if (YEAR != AppConstants.YEAR) copyright_years = copyright_years + " - " + YEAR; 72 | return ( 73 | 74 |
75 | 76 | 77 |
{this.props.children}
78 | 79 | 82 |
83 |
84 | ) 85 | } 86 | } 87 | 88 | // Important! 89 | Site.childContextTypes = { 90 | muiTheme: PropTypes.object 91 | }; 92 | 93 | var injectTapEventPlugin = require("react-tap-event-plugin"); 94 | //Needed for onTouchTap 95 | //Can go away when react 1.0 release 96 | //Check this repo: 97 | //https://github.com/zilverline/react-tap-event-plugin 98 | injectTapEventPlugin(); 99 | 100 | export default supplyFluxContext(alt)(Site) 101 | -------------------------------------------------------------------------------- /src/js/components/Splash.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppConstants = require('constants/AppConstants'); 3 | import {Link} from 'react-router'; 4 | import GoogleLoginCompat from 'components/common/GoogleLoginCompat'; 5 | import {RaisedButton, Snackbar} from 'material-ui'; 6 | import {G_OAUTH_CLIENT_ID, DEV_G_OAUTH_CLIENT_ID, DEV_GOOGLE_API_KEY, GOOGLE_API_KEY} from 'constants/client_secrets'; 7 | var client_secrets = require('constants/client_secrets'); 8 | 9 | export default class Splash extends React.Component { 10 | static defaultProps = { 11 | signing_in: false 12 | } 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | }; 17 | } 18 | 19 | success(gUser) { 20 | } 21 | 22 | fail(res) { 23 | console.log(res) 24 | } 25 | 26 | render() { 27 | let SITENAME = AppConstants.SITENAME; 28 | let {user, signing_in} = this.props; 29 | let snack_message = "Signing you in..."; 30 | let cta = user ? `Welcome back to ${SITENAME}` : `Welcome to ${SITENAME}`; 31 | let oauth_client_id = constants.dev ? client_secrets.DEV_G_OAUTH_CLIENT_ID || client_secrets.G_OAUTH_CLIENT_ID : client_secrets.G_OAUTH_CLIENT_ID 32 | return ( 33 |
34 | 35 |
36 | 37 |

{cta}

38 | 39 |

{ AppConstants.TAGLINE }

40 | 41 | 44 | 45 | 59 | 60 |
61 | 62 |
63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/js/components/TaskHistory.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TaskLI = require('components/list_items/TaskLI'); 3 | var util = require('utils/util'); 4 | import {changeHandler} from 'utils/component-utils'; 5 | import {clone} from 'lodash'; 6 | var FetchedList = require('components/common/FetchedList'); 7 | 8 | @changeHandler 9 | export default class TaskHistory extends React.Component { 10 | static defaultProps = {}; 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | componentDidMount() { 16 | util.set_title("Task History"); 17 | } 18 | 19 | render_task(t) { 20 | return 25 | } 26 | 27 | render() { 28 | let params = {with_archived: 1} 29 | return ( 30 |
31 | 32 |

Task History

33 | 34 | 42 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | module.exports = TaskHistory; 49 | -------------------------------------------------------------------------------- /src/js/components/TrackingHistory.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var util = require('utils/util'); 3 | import {changeHandler} from 'utils/component-utils'; 4 | import {clone, isEqual} from 'lodash'; 5 | var FetchedList = require('components/common/FetchedList'); 6 | import {DatePicker, FontIcon, Paper, ListItem} from 'material-ui' 7 | 8 | @changeHandler 9 | export default class TrackingHistory extends React.Component { 10 | static defaultProps = {}; 11 | constructor(props) { 12 | super(props); 13 | let init_to = new Date() 14 | let init_from = new Date() 15 | init_from.setDate(init_from.getDate() - 7) 16 | this.state = { 17 | form: { 18 | date_from: init_from, 19 | date_to: init_to, 20 | }, 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | util.set_title("Tracking History"); 26 | } 27 | 28 | componentDidUpdate(prevProps, prevState) { 29 | let filter_change = !isEqual(prevState.form, this.state.form) 30 | if (filter_change) { 31 | // TODO: This is not firing 32 | this.refs.tds.refresh(); 33 | } 34 | } 35 | 36 | render_td(td) { 37 | let pt = td.iso_date 38 | let st = [] 39 | Object.keys(td.data).map((key) => { 40 | let val = td.data[key]; 41 | st.push({key}: {val}) 42 | }) 43 | return today} key={td.id} primaryText={pt} secondaryText={st} /> 44 | } 45 | 46 | render() { 47 | let {form} = this.state; 48 | let params = clone(form); 49 | if (form.date_from) params.date_from = util.printDateObj(form.date_from); 50 | if (form.date_to) params.date_to = util.printDateObj(form.date_to); 51 | return ( 52 |
53 | 54 |

Tracking History

55 | 56 | 57 |
58 |
59 | 64 |
65 |
66 | 71 |
72 |
73 |
74 | 75 | 84 | 85 |
86 | ); 87 | } 88 | } 89 | 90 | module.exports = TrackingHistory; 91 | -------------------------------------------------------------------------------- /src/js/components/admin/AdminAgent.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var util = require('utils/util'); 4 | var UserStore = require('stores/UserStore'); 5 | import {RaisedButton, TextField} from 'material-ui'; 6 | var api = require('utils/api'); 7 | import connectToStores from 'alt-utils/lib/connectToStores'; 8 | var toastr = require('toastr'); 9 | import {changeHandler} from 'utils/component-utils'; 10 | 11 | @connectToStores 12 | @changeHandler 13 | export default class AdminAgent extends React.Component { 14 | static defaultProps = {}; 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | form: { 19 | message: '' 20 | } 21 | }; 22 | } 23 | 24 | static getStores() { 25 | return [UserStore]; 26 | } 27 | 28 | static getPropsFromStores() { 29 | return UserStore.getState(); 30 | } 31 | 32 | componentDidMount() { 33 | 34 | } 35 | 36 | send() { 37 | let {form} = this.state; 38 | api.post("/api/agent/spoof", form, (res) => { 39 | 40 | }); 41 | } 42 | 43 | render() { 44 | let {form} = this.state; 45 | return ( 46 |
47 | 48 |

Test Agent

49 | 50 | 51 | 52 |
53 | ); 54 | } 55 | }; 56 | 57 | module.exports = AdminAgent; 58 | -------------------------------------------------------------------------------- /src/js/components/analysis/AnalysisMisc.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | import {Bar, Line} from "react-chartjs-2"; 4 | import connectToStores from 'alt-utils/lib/connectToStores'; 5 | import {IconMenu, MenuItem, FontIcon, IconButton} from 'material-ui' 6 | var api = require('utils/api'); 7 | import {Link} from 'react-router' 8 | import {get} from 'lodash'; 9 | import {findItemById} from 'utils/store-utils'; 10 | 11 | @connectToStores 12 | export default class AnalysisMisc extends React.Component { 13 | static defaultProps = { 14 | tracking_days: [] 15 | }; 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | readables_read: [] 20 | }; 21 | } 22 | 23 | static getStores() { 24 | return []; 25 | } 26 | 27 | static getPropsFromStores() { 28 | return {}; 29 | } 30 | 31 | componentDidMount() { 32 | let since = this.props.iso_dates[0]; 33 | api.get("/api/readable", {read: 1, since: since}, (res) => { 34 | console.log(res.readables); 35 | this.setState({readables_read: res.readables}); 36 | }) 37 | } 38 | 39 | productivity_data() { 40 | let {tracking_days, iso_dates, user} = this.props; 41 | let {readables_read} = this.state; 42 | let labels = []; 43 | let commit_data = []; 44 | let reading_data = []; 45 | let date_to_read_count = {}; 46 | let vars = []; 47 | if (user.settings) vars = get(user.settings, ['tracking', 'chart_vars'], []); 48 | let var_data = {}; // var.name -> data array 49 | readables_read.forEach((r) => { 50 | if (!date_to_read_count[r.date_read]) date_to_read_count[r.date_read] = 0; 51 | date_to_read_count[r.date_read] += 1; 52 | }); 53 | iso_dates.forEach((date) => { 54 | let td = findItemById(tracking_days, date, 'iso_date'); 55 | commit_data.push(td ? td.data.commits : 0); 56 | reading_data.push(date_to_read_count[date] || 0); 57 | vars.forEach((v) => { 58 | if (!var_data[v.name]) var_data[v.name] = []; 59 | let val = 0; 60 | if (td) val = td.data[v.name] || 0; 61 | if (v.mult) val *= v.mult; 62 | var_data[v.name].push(val); 63 | }); 64 | labels.push(date); 65 | }); 66 | // Align reading counts with tracking days 67 | let datasets = [ 68 | { 69 | label: "Commits", 70 | data: commit_data, 71 | backgroundColor: '#44ff44' 72 | }, 73 | { 74 | label: "Items Read", 75 | data: reading_data, 76 | backgroundColor: '#E846F9' 77 | } 78 | ]; 79 | vars.forEach((v) => { 80 | if (var_data[v.name]) { 81 | datasets.push({ 82 | label: v.label, 83 | data: var_data[v.name], 84 | backgroundColor: v.color || '#FFFFFF' 85 | }) 86 | } 87 | }) 88 | console.log(datasets); 89 | let pdata = { 90 | labels: labels, 91 | datasets: datasets 92 | }; 93 | return pdata; 94 | } 95 | 96 | render() { 97 | let {loaded, tracking_days} = this.props; 98 | let today = new Date(); 99 | let trackingData = this.productivity_data(); 100 | let trackingOps = { 101 | scales: { 102 | xAxes: [{ 103 | type: 'time', 104 | time: { 105 | unit: 'day' 106 | } 107 | }], 108 | yAxes: [{ 109 | ticks: { 110 | min: 0 111 | } 112 | }], 113 | } 114 | }; 115 | if (!loaded) return null; 116 | 117 | return ( 118 |
119 | 120 | more_vert}> 121 | list} /> 122 | 123 | 124 |

Tracking

125 | 126 | 127 | 128 |
129 | ); 130 | } 131 | } 132 | 133 | module.exports = AnalysisMisc; -------------------------------------------------------------------------------- /src/js/components/analysis/AnalysisTasks.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var util = require('utils/util'); 3 | import {Bar, Line} from "react-chartjs-2"; 4 | import Select from 'react-select' 5 | import {changeHandler} from 'utils/component-utils'; 6 | import connectToStores from 'alt-utils/lib/connectToStores'; 7 | 8 | @connectToStores 9 | @changeHandler 10 | export default class AnalysisTasks extends React.Component { 11 | static defaultProps = { 12 | goals: {} 13 | }; 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | form: { 18 | chart_type: 'count' // ['count', 'sessions', 'time'] 19 | }, 20 | }; 21 | } 22 | 23 | static getStores() { 24 | return []; 25 | } 26 | 27 | static getPropsFromStores() { 28 | return {}; 29 | } 30 | 31 | componentDidMount() { 32 | 33 | } 34 | 35 | get_task_value(t) { 36 | let {form} = this.state; 37 | return { 38 | 'count': 1, 39 | 'sessions': t.timer_complete_sess, 40 | 'time': parseInt(t.timer_total_ms / 1000 / 60) 41 | }[form.chart_type]; 42 | } 43 | 44 | task_data() { 45 | 46 | let {iso_dates, tasks} = this.props; 47 | let completed_on_time = []; 48 | let completed_late = []; 49 | let not_completed = []; 50 | let DUE_BUFFER = 1000*60*60*2; // 2 hrs 51 | iso_dates.forEach((iso_date) => { 52 | let tasks_due_on_day = tasks.filter((t) => { 53 | return util.printDate(t.ts_due) == iso_date; 54 | }); 55 | let on_time_value = 0; 56 | let late_value = 0; 57 | let incomplete_value = 0; 58 | tasks_due_on_day.forEach((t) => { 59 | let done = t.done; 60 | let value = this.get_task_value(t); 61 | if (done) { 62 | let on_time = t.ts_done <= t.ts_due + DUE_BUFFER; 63 | if (on_time) on_time_value += value; 64 | else late_value += value; 65 | } else { 66 | incomplete_value += value; 67 | } 68 | }); 69 | completed_on_time.push(on_time_value); 70 | completed_late.push(late_value); 71 | not_completed.push(incomplete_value); 72 | }) 73 | let data = { 74 | labels: iso_dates, 75 | datasets: [ 76 | { 77 | label: "Completed", 78 | data: completed_on_time, 79 | backgroundColor: '#4FFF7A' 80 | }, 81 | { 82 | label: "Completed Late", 83 | data: completed_late, 84 | backgroundColor: '#DBFE5E' 85 | }, 86 | { 87 | label: "Not Completed", 88 | data: not_completed, 89 | backgroundColor: '#F7782D' 90 | } 91 | ] 92 | }; 93 | return data; 94 | } 95 | 96 | render() { 97 | let {form} = this.state; 98 | let taskData = this.task_data() 99 | let taskOptions = { 100 | scales: { 101 | yAxes: [{ 102 | stacked: true 103 | }], 104 | xAxes: [{ 105 | stacked: true 106 | }] 107 | } 108 | } 109 | let opts = [ 110 | {value: 'count', label: "Task Count"}, 111 | {value: 'sessions', label: "Completed Sessions"}, 112 | {value: 'time', label: "Logged Time (minutes)"} 113 | ] 114 | return ( 115 |
116 | 117 | 118 |

Top Tasks

119 | 120 |
121 |
122 |
123 | 124 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/js/components/list_items/JournalLI.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | import {Paper, IconButton} from 'material-ui'; 3 | import PropTypes from 'prop-types'; 4 | import {changeHandler} from 'utils/component-utils'; 5 | var ProgressLine = require('components/common/ProgressLine'); 6 | 7 | 8 | @changeHandler 9 | export default class JournalLI extends React.Component { 10 | static propTypes = { 11 | journal: PropTypes.object, 12 | questions: PropTypes.array, 13 | onEditClick: PropTypes.func 14 | } 15 | 16 | static defaultProps = { 17 | questions: [], 18 | journal: null, 19 | } 20 | 21 | constructor(props) { 22 | super(props); 23 | this.state = {} 24 | } 25 | 26 | handle_edit_click() { 27 | this.props.onEditClick(); 28 | } 29 | 30 | render() { 31 | let {journal, questions} = this.props; 32 | let data = journal.data; 33 | let responses = questions.map((q, i) => { 34 | let q_response = data[q.name]; 35 | let q_response_rendered = "N/A"; 36 | let reverse = q.value_reverse || false; 37 | let rt = q.response_type; 38 | let min_color = reverse ? "#4FECF9" : "#FC004E" 39 | let color = reverse ? "#FC004E" : "#4FECF9" 40 | if (q_response != null) { 41 | if (rt == 'text' || rt == 'number_oe') q_response_rendered =
{q_response}
; 42 | else if (rt == 'number' || rt == 'slider') q_response_rendered = 43 | } 44 | return ( 45 |
46 | 47 | {q_response_rendered} 48 |
49 | ); 50 | }); 51 | return ( 52 | 53 | 54 |

55 | { journal.iso_date } 56 | edit 57 |

58 | 59 | 60 |
61 | { responses } 62 |
63 |
64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/js/components/list_items/QuoteLI.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | import {ListItem, FontIcon, 3 | IconMenu, MenuItem, IconButton} from 'material-ui'; 4 | var api = require('utils/api'); 5 | import PropTypes from 'prop-types'; 6 | import {changeHandler} from 'utils/component-utils'; 7 | var util = require('utils/util'); 8 | 9 | @changeHandler 10 | export default class QuoteLI extends React.Component { 11 | static propTypes = { 12 | quote: PropTypes.object 13 | } 14 | 15 | static defaultProps = { 16 | quote: null 17 | } 18 | 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | expanded: false, 23 | deleted: false 24 | }; 25 | } 26 | 27 | toggle_expanded() { 28 | this.setState({expanded: !this.state.expanded}); 29 | } 30 | 31 | link_readable() { 32 | let {quote} = this.props; 33 | api.post("/api/quote/action", {action: 'link_readable', id: quote.id}); 34 | } 35 | 36 | delete_quote() { 37 | let {quote} = this.props; 38 | api.post("/api/quote/delete", {id: quote.id}, (res) => { 39 | this.setState({deleted: true}); 40 | }); 41 | } 42 | 43 | render() { 44 | let {quote} = this.props; 45 | let {expanded, deleted} = this.state; 46 | if (deleted) return
; 47 | let icon = expanded ? 'expand_less' : 'expand_more'; 48 | let linked_readable = quote.readable != null; 49 | let src = quote.source; 50 | if (linked_readable) src = { src } 51 | let subs = [src]; 52 | if (quote.location) subs.push( · {quote.location}); 53 | if (quote.iso_date) subs.push( · {quote.iso_date}); 54 | let text = expanded ? {quote.content} : util.truncate(quote.content, 120); 55 | let menu = ( 56 | more_vert}> 57 | {icon}} key="toggle" onClick={this.toggle_expanded.bind(this)}>Toggle expanded 58 | search} key="lookup" onClick={this.link_readable.bind(this)}>Lookup and link readable 59 | delete} key="delete" onClick={this.delete_quote.bind(this)}>Delete quote 60 | 61 | ); 62 | return ( 63 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/js/config/Routes.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Site = require('components/Site'); 4 | var App = require('components/App'); 5 | var Dashboard = require('components/Dashboard'); 6 | var Timeline = require('components/Timeline'); 7 | var Splash = require('components/Splash'); 8 | var About = require('components/About'); 9 | var Privacy = require('components/Privacy'); 10 | var Auth = require('components/Auth'); 11 | var Settings = require('components/Settings'); 12 | var Analysis = require('components/Analysis'); 13 | var Reading = require('components/Reading'); 14 | var JournalHistory = require('components/JournalHistory'); 15 | var TaskHistory = require('components/TaskHistory'); 16 | var HabitHistory = require('components/HabitHistory'); 17 | var TrackingHistory = require('components/TrackingHistory'); 18 | var ProjectHistory = require('components/ProjectHistory'); 19 | var Integrations = require('components/Integrations'); 20 | var Reports = require('components/Reports'); 21 | var Feedback = require('components/Feedback'); 22 | var AdminAgent = require('components/admin/AdminAgent'); 23 | 24 | // Analysis 25 | var AnalysisGoals = require('components/analysis/AnalysisGoals'); 26 | var AnalysisJournals = require('components/analysis/AnalysisJournals'); 27 | var AnalysisTasks = require('components/analysis/AnalysisTasks'); 28 | var AnalysisHabits = require('components/analysis/AnalysisHabits'); 29 | var AnalysisSnapshot = require('components/analysis/AnalysisSnapshot'); 30 | var AnalysisMisc = require('components/analysis/AnalysisMisc'); 31 | 32 | var NotFound = require('components/NotFound'); 33 | 34 | var Router = require('react-router'); 35 | 36 | var Route = Router.Route; 37 | var IndexRedirect = Router.IndexRedirect; 38 | 39 | module.exports = ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); -------------------------------------------------------------------------------- /src/js/config/alt.js: -------------------------------------------------------------------------------- 1 | var Alt = require('alt'); 2 | var alt = new Alt(); 3 | 4 | module.exports = alt; -------------------------------------------------------------------------------- /src/js/config/history.js: -------------------------------------------------------------------------------- 1 | import createBrowserHistory from 'history/lib/createBrowserHistory' 2 | export default createBrowserHistory() -------------------------------------------------------------------------------- /src/js/constants/Styles.js: -------------------------------------------------------------------------------- 1 | var Styles = { 2 | // Unused currently 3 | Dialog: { 4 | contentStyle: { 5 | width: '100%', 6 | maxWidth: '550px', 7 | maxHeight: '100% !important' 8 | }, 9 | bodyStyle: { 10 | maxHeight: '100% !important' 11 | }, 12 | style: { 13 | paddingTop: '0 !important', 14 | marginTop: '-65px !important', 15 | bottom: '0 !important', 16 | overflow: 'scroll !important', 17 | height: 'auto !important' 18 | } 19 | } 20 | } 21 | 22 | module.exports = Styles; -------------------------------------------------------------------------------- /src/js/constants/client_secrets.template.js: -------------------------------------------------------------------------------- 1 | var client_secrets = { 2 | 3 | // Match with settings/secrets.py and GCP console 4 | G_OAUTH_CLIENT_ID: "######.XXXXXXXXXXXX.apps.googleusercontent.com", 5 | DEV_G_OAUTH_CLIENT_ID: "######.XXXXXXXXXXXX.apps.googleusercontent.com", 6 | GOOGLE_API_KEY: "XXXXXXXXXXXX", 7 | 8 | // Goodreads 9 | GR_API_KEY: "" 10 | }; 11 | 12 | module.exports = client_secrets; -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactDOM = require('react-dom'); 3 | import { Router, browserHistory } from 'react-router'; 4 | // Browser ES6 Polyfill 5 | require('babel/polyfill'); 6 | var routes = require('config/Routes'); 7 | ReactDOM.render(, document.getElementById('app')); 8 | -------------------------------------------------------------------------------- /src/js/sources/ProjectSource.js: -------------------------------------------------------------------------------- 1 | 2 | var ProjectActions = require('actions/ProjectActions'); 3 | var api = require('utils/api'); 4 | import {findIndexById} from 'utils/store-utils'; 5 | 6 | const _PROJECT_API_URL = '/api/project'; 7 | const _construct_api_url = (url_part) => `${_PROJECT_API_URL}/${url_part}`; 8 | 9 | const ProjectSource = { 10 | 11 | fetchProjects: { 12 | 13 | remote(state) { 14 | return api.get("/api/project/active", {}) 15 | }, 16 | 17 | // this function checks in our local cache first 18 | // if the value is present it'll use that instead (optional). 19 | local(state) { 20 | if (state.loaded) { 21 | return state.projects 22 | } 23 | }, 24 | 25 | // here we setup some actions to handle our response 26 | loading: ProjectActions.fetchingProjects, // (optional) 27 | success: ProjectActions.gotProjects, // (required) 28 | error: ProjectActions.fetchingProjectsFailed, // (required) 29 | 30 | // shouldFetch(state) { 31 | // return true 32 | // } 33 | }, 34 | 35 | updateProject: { 36 | remote(state, params) { 37 | return api.post("/api/project", params) 38 | }, 39 | 40 | loading: ProjectActions.updatingProject, 41 | success: ProjectActions.updatedProject, 42 | error: ProjectActions.updatingProjectFailed 43 | } 44 | 45 | }; 46 | 47 | export default ProjectSource; 48 | -------------------------------------------------------------------------------- /src/js/stores/ProjectStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('config/alt'); 2 | import ProjectActions from 'actions/ProjectActions'; 3 | import ProjectSource from 'sources/ProjectSource'; 4 | import {findIndexById} from 'utils/store-utils'; 5 | 6 | class ProjectStore { 7 | constructor() { 8 | this.bindActions(ProjectActions); 9 | this.projects = [] 10 | this.loaded = false 11 | this.working = false 12 | this.registerAsync(ProjectSource) 13 | 14 | this.exportPublicMethods({ 15 | getProjectByTitle: this.getProjectByTitle, 16 | getProjectById: this.getProjectById, 17 | }) 18 | } 19 | 20 | onFetchingProjects() { 21 | this.working = true 22 | } 23 | 24 | onFetchingProjectsFailed() { 25 | this.working = false 26 | } 27 | 28 | onGotProjects({projects}) { 29 | this.projects = projects 30 | this.working = false 31 | this.loaded = true 32 | } 33 | 34 | onUpdatingProject() { 35 | this.working = true 36 | } 37 | 38 | onUpdatingProjectFailed() { 39 | this.working = false 40 | } 41 | 42 | onUpdatedProject(project) { 43 | let idx = findIndexById(this.projects, project.id, 'id'); 44 | if (idx > -1) this.projects[idx] = project; 45 | else this.projects.push(project); 46 | this.working = false 47 | } 48 | 49 | // Public 50 | 51 | getProjectByTitle(title) { 52 | let projects = this.getState().projects 53 | let idx = findIndexById(projects, title, 'title'); 54 | if (idx > -1) return projects[idx] 55 | } 56 | 57 | getProjectById(id) { 58 | let projects = this.getState().projects 59 | let idx = findIndexById(projects, id, 'id'); 60 | if (idx > -1) return projects[idx] 61 | } 62 | 63 | } 64 | 65 | module.exports = (alt.createStore(ProjectStore, "ProjectStore")); -------------------------------------------------------------------------------- /src/js/stores/TaskStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('config/alt'); 2 | import TaskActions from 'actions/TaskActions'; 3 | 4 | 5 | class TaskStore { 6 | constructor() { 7 | this.bindActions(TaskActions); 8 | this.dialog_open = false 9 | } 10 | 11 | onOpenTaskDialog() { 12 | this.dialog_open = true 13 | } 14 | 15 | onCloseTaskDialog() { 16 | this.dialog_open = false 17 | } 18 | 19 | } 20 | 21 | module.exports = (alt.createStore(TaskStore, "TaskStore")); -------------------------------------------------------------------------------- /src/js/stores/UserStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('config/alt'); 2 | var UserActions = require('actions/UserActions'); 3 | import { browserHistory } from 'react-router'; 4 | var AppConstants = require('constants/AppConstants'); 5 | 6 | class UserStore { 7 | constructor() { 8 | this.bindActions(UserActions); 9 | this.user = null; 10 | this.error = null; 11 | 12 | this.exportPublicMethods({ 13 | get_user: this.get_user, 14 | admin: this.admin, 15 | plugin_enabled: this.plugin_enabled, 16 | request_scopes: this.request_scopes 17 | }); 18 | } 19 | 20 | storeUser(user) { 21 | this.user = user; 22 | this.error = null; 23 | console.log("Stored user "+user.email); 24 | // api.updateToken(user.token); 25 | localStorage.setItem(AppConstants.USER_STORAGE_KEY, JSON.stringify(user)); 26 | } 27 | 28 | request_scopes(scopes_array, cb, cb_fail) { 29 | // "An ID token has replaced OAuth2 access tokens and scopes." 30 | // See https://developers.google.com/identity/gsi/web/guides/migration 31 | let granted_scopes = []; 32 | console.log('granted', granted_scopes); 33 | let scopes_needed = []; 34 | scopes_array.forEach((scope) => { 35 | if (!granted_scopes || granted_scopes.indexOf(scope) == -1) scopes_needed.push(scope); 36 | }); 37 | if (scopes_needed.length > 0) { 38 | guser.grant({'scope': scopes_needed.join(' ')}).then(cb, cb_fail); 39 | } else { 40 | console.log('we have all requested scopes'); 41 | cb(); 42 | } 43 | } 44 | 45 | loadLocalUser() { 46 | var user; 47 | try { 48 | switch (AppConstants.PERSISTENCE) { 49 | case "bootstrap": 50 | alt.bootstrap(JSON.stringify(alt_bootstrap)); 51 | break; 52 | } 53 | 54 | } finally { 55 | if (this.user) { 56 | console.log("Successfully loaded user " + this.user.email); 57 | } 58 | } 59 | } 60 | 61 | clearUser() { 62 | console.log("Clearing user after signout"); 63 | this.user = null; 64 | localStorage.removeItem(AppConstants.USER_STORAGE_KEY); 65 | } 66 | 67 | onLogout(data) { 68 | if (data.success) { 69 | this.clearUser(); 70 | this.error = null; 71 | console.log('Signed out of Flow'); 72 | browserHistory.push('/app'); 73 | } 74 | } 75 | 76 | onUpdate(data) { 77 | this.storeUser(data.user); 78 | if (data.oauth_uri != null) { 79 | window.location = data.oauth_uri; 80 | } 81 | } 82 | 83 | // Public 84 | 85 | get_user(uid) { 86 | var u = this.getState().users[uid]; 87 | return u; 88 | } 89 | 90 | plugin_enabled(plugin) { 91 | let plugins = this.getState().user.plugins; 92 | return plugins != null && plugins.indexOf(plugin) > -1; 93 | } 94 | 95 | admin() { 96 | return this.getState().user.level == AppConstants.USER_ADMIN; 97 | } 98 | 99 | } 100 | 101 | module.exports = alt.createStore(UserStore, 'UserStore'); 102 | -------------------------------------------------------------------------------- /src/js/utils/action-utils.js: -------------------------------------------------------------------------------- 1 | // import {isFunction} from 'lodash'; 2 | // import StatusActions from 'actions/status-actions'; 3 | import UserActions from '../actions/UserActions'; 4 | var $ = require('jquery'); 5 | 6 | 7 | export default { 8 | networkAction: async function(context, method, ...params) { 9 | console.log('networkAction...' + method) 10 | try { 11 | // StatusActions.started(); 12 | const response = await method.apply(context, params); 13 | // const data = isFunction(response) ? response().data : response.data; 14 | context.dispatch(response().data); 15 | // StatusActions.done(); 16 | } catch (err) { 17 | console.error(err); 18 | if (err.status === 401) { 19 | UserActions.logout(); 20 | } 21 | else { 22 | // StatusActions.failed({config: err.config, action: context.actionDetails}); 23 | } 24 | } 25 | }, 26 | 27 | post: function(context, path, data) { 28 | try { 29 | // StatusActions.started(); 30 | $.post(path, data, function(res) { 31 | context.dispatch(res); 32 | }, 'json'); 33 | // StatusActions.done(); 34 | } catch (err) { 35 | console.error(err); 36 | if (err.status === 401) { 37 | UserActions.logout(); 38 | } 39 | else { 40 | // StatusActions.failed({config: err.config, action: context.actionDetails}); 41 | } 42 | } 43 | }, 44 | 45 | get: function(context, path, data) { 46 | try { 47 | // StatusActions.started(); 48 | $.getJSON(path, data, function(res) { 49 | context.dispatch(res); 50 | }); 51 | // StatusActions.done(); 52 | } catch (err) { 53 | console.error(err); 54 | if (err.status === 401) { 55 | UserActions.logout(); 56 | } 57 | else { 58 | // StatusActions.failed({config: err.config, action: context.actionDetails}); 59 | } 60 | } 61 | } 62 | 63 | }; -------------------------------------------------------------------------------- /src/js/utils/api.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var AppConstants = require('constants/AppConstants'); 3 | var toastr = require('toastr'); 4 | 5 | var api = { 6 | 7 | post: function(url, data, success, fail, opts) { 8 | var no_success_bool = opts && opts.no_success_bool; 9 | return $.post(url, data, function(res, status, jqxhr) { 10 | if (res) { 11 | if (res.message) { 12 | if (res.success) toastr.success(res.message); 13 | else toastr.error(res.message); 14 | } 15 | if ((res.success || no_success_bool) && typeof(success)==='function') success(res); 16 | if (!no_success_bool && !res.success) { 17 | if (typeof(fail)==='function') fail(res); 18 | } 19 | } 20 | }, 'json').fail(function(jqxhr, textStatus, errorThrown) { 21 | var status = jqxhr.status; 22 | if (status == 401) { 23 | toastr.error("You are signed out"); 24 | localStorage.removeItem(AppConstants.USER_STORAGE_KEY); 25 | window.location = "/app"; 26 | } 27 | if (typeof(fail) === 'function') fail(); 28 | }); 29 | }, 30 | 31 | get: function(url, data, success, fail, opts) { 32 | var no_toast = opts && opts.no_toast; 33 | return $.getJSON(url, data, function(res, _status, jqxhr) { 34 | if (res) { 35 | if (res.message && !no_toast) { 36 | if (res.success) toastr.success(res.message); 37 | else toastr.error(res.message); 38 | } 39 | if (res.success && typeof(success)==='function') success(res); 40 | if (!res.success) { 41 | if (typeof(fail)==='function') fail(res); 42 | } 43 | } 44 | }).fail(function(jqxhr, textStatus, errorThrown) { 45 | var status = jqxhr.status; 46 | if (status == 401) { 47 | toastr.error("You are signed out"); 48 | localStorage.removeItem(AppConstants.USER_STORAGE_KEY); 49 | window.location = "/app"; 50 | } 51 | toastr.error("An unknown error has occurred"); 52 | if (typeof(fail) === 'function') fail(); 53 | }); 54 | } 55 | 56 | } 57 | 58 | export default api; -------------------------------------------------------------------------------- /src/js/utils/component-utils.js: -------------------------------------------------------------------------------- 1 | var util = require('utils/util'); 2 | import {clone, merge} from 'lodash' 3 | 4 | export default { 5 | changeHandler: function(target) { 6 | target.prototype.changeHandlerVal = function(key, attr, value) { 7 | var state = {}; 8 | if (key != null) { 9 | state[key] = this.state[key] || {}; 10 | state[key][attr] = value; 11 | } else { 12 | state[attr] = value; 13 | } 14 | state.lastChange = util.nowTimestamp(); // ms 15 | this.setState(state); 16 | }; 17 | target.prototype.changeHandler = function(key, attr, event) { 18 | this.changeHandlerVal(key, attr, event.currentTarget.value); 19 | }; 20 | target.prototype.changeHandlerDropDown = function(key, attr, event, index, value) { 21 | this.changeHandlerVal(key, attr, value); 22 | }; 23 | target.prototype.changeHandlerSlider = function(key, attr, event, value) { 24 | this.changeHandlerVal(key, attr, value); 25 | }; 26 | target.prototype.changeHandlerEventValue = function(key, attr, event, value) { 27 | this.changeHandlerVal(key, attr, value); 28 | }; 29 | target.prototype.changeHandlerToggle = function(key, attr, value) { 30 | var state = {}; 31 | state[key] = this.state[key] || {}; 32 | state[key][attr] = !state[key][attr]; 33 | state.lastChange = util.nowTimestamp(); // ms 34 | this.setState(state); 35 | }; 36 | target.prototype.changeHandlerMultiVal = function(key, attr, value) { 37 | this.changeHandlerVal(key, attr, value.map((vo) => {return vo.value;})); 38 | }; 39 | target.prototype.changeHandlerValWithAdditions = function(key, attr, additional_state_updates, value) { 40 | var state = {}; 41 | state[key] = this.state[key] || {}; 42 | state[key][attr] = value; 43 | state.lastChange = util.nowTimestamp(); // ms 44 | if (additional_state_updates != null) merge(state, additional_state_updates); 45 | this.setState(state); 46 | }; 47 | target.prototype.changeHandlerNilVal = function(key, attr, nill, value) { 48 | this.changeHandlerVal(key, attr, value); 49 | }; 50 | return target; 51 | }, 52 | authDecorator: function(target) { 53 | target.willTransitionTo = function(transition) { 54 | if (!localStorage.user) { 55 | transition.redirect('login'); 56 | } 57 | }; 58 | return target; 59 | } 60 | }; -------------------------------------------------------------------------------- /src/js/utils/store-utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | removeItemsById: function(collection, id_list, _id_prop) { 3 | var id_prop = _id_prop || "id"; 4 | return collection.filter(function(x) { return id_list.indexOf(x[id_prop]) == -1; } ) 5 | }, 6 | findItemById: function(collection, id, _id_prop) { 7 | var id_prop = _id_prop || "id"; 8 | return collection.find(x => x && x[id_prop] === id); 9 | }, 10 | findIndexById: function(collection, id, _id_prop) { 11 | var id_prop = _id_prop || "id"; 12 | var ids = collection.map(function(x) {return (x != null) ? x[id_prop] : null; }); 13 | return ids.indexOf(id); 14 | } 15 | }; -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /static/bootstrap/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/favicon-96x96.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/favicon.ico -------------------------------------------------------------------------------- /static/flow-agent/agent.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Flow dashboard interaction for Google Home / Google Assistant", 3 | "language": "en", 4 | "enabledDomainFeatures": [ 5 | "smalltalk-domain-on", 6 | "smalltalk-fulfillment-on", 7 | "smalltalk-domain-new" 8 | ], 9 | "googleAssistant": { 10 | "googleAssistantCompatible": true, 11 | "invocationName": "flow dashboard", 12 | "project": "genzai-app", 13 | "welcomeIntentSignInRequired": true, 14 | "startIntents": [ 15 | { 16 | "intentId": "7fb0c1c5-4935-4f8a-9d85-f5f4a71829d7", 17 | "signInRequired": true 18 | }, 19 | { 20 | "intentId": "b2019cfc-f53b-4c36-80a0-32ec1c742f22", 21 | "signInRequired": true 22 | }, 23 | { 24 | "intentId": "308e5379-7d79-42dd-b66c-7c1d44e1c2fd", 25 | "signInRequired": true 26 | }, 27 | { 28 | "intentId": "8633d4e9-a3b2-49ba-bd78-d781ca04b280", 29 | "signInRequired": true 30 | } 31 | ], 32 | "systemIntents": [], 33 | "endIntentIds": [ 34 | "308e5379-7d79-42dd-b66c-7c1d44e1c2fd", 35 | "10f72820-b50b-44e5-bd4d-0db1e55d43a1", 36 | "b2019cfc-f53b-4c36-80a0-32ec1c742f22", 37 | "7fb0c1c5-4935-4f8a-9d85-f5f4a71829d7", 38 | "6d145e1f-da03-4a9d-b070-ffec1ed27f51", 39 | "d721fe0f-48cc-411d-8b6d-0b31c38e52c8", 40 | "ec792743-3a65-4dc8-b378-c1a25e0f9b5f", 41 | "9b9973d1-d279-4b14-962a-729e2973ee9f" 42 | ], 43 | "oAuthLinking": { 44 | "required": false, 45 | "authorizationUrl": "https://flowdash.co/auth/google", 46 | "grantType": "IMPLICIT_GRANT" 47 | }, 48 | "voiceType": "FEMALE_1", 49 | "capabilities": [], 50 | "protocolVersion": "V2" 51 | }, 52 | "defaultTimezone": "", 53 | "webhook": { 54 | "url": "https://flowdash.co/api/agent/apiai/request", 55 | "username": "", 56 | "headers": { 57 | "Auth-Key": "KEY", 58 | "": "" 59 | }, 60 | "available": true, 61 | "useForDomains": true 62 | }, 63 | "isPrivate": true, 64 | "customClassifierMode": "use.after", 65 | "mlMinConfidence": 0.2 66 | } -------------------------------------------------------------------------------- /static/flow-agent/customDomainsResponses.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "smalltalk", 3 | "customResponses": [ 4 | { 5 | "action": "smalltalk.agent.acquaintance", 6 | "parameters": [], 7 | "customAnswers": [ 8 | "I\u0027m Flow", 9 | "My name\u0027s Flow" 10 | ] 11 | }, 12 | { 13 | "action": "smalltalk.agent.annoying", 14 | "parameters": [], 15 | "customAnswers": [ 16 | "Sorry!", 17 | "I\u0027m still learning, sorry about that", 18 | "Woops -- I don\u0027t mean to be" 19 | ] 20 | }, 21 | { 22 | "action": "smalltalk.agent.answer_my_question", 23 | "parameters": [], 24 | "customAnswers": [ 25 | "Shrug" 26 | ] 27 | }, 28 | { 29 | "action": "smalltalk.agent.bad", 30 | "parameters": [], 31 | "customAnswers": [ 32 | "Sorry!", 33 | "Sorry, I\u0027ll try harder" 34 | ] 35 | }, 36 | { 37 | "action": "smalltalk.agent.be_clever", 38 | "parameters": [], 39 | "customAnswers": [ 40 | "I\u0027m working on it" 41 | ] 42 | }, 43 | { 44 | "action": "smalltalk.agent.busy", 45 | "parameters": [], 46 | "customAnswers": [ 47 | "No, I\u0027m free to help", 48 | "Never for you" 49 | ] 50 | }, 51 | { 52 | "action": "smalltalk.agent.can_you_help", 53 | "parameters": [], 54 | "customAnswers": [ 55 | "Sure, how can I help?" 56 | ] 57 | }, 58 | { 59 | "action": "smalltalk.agent.chatbot", 60 | "parameters": [], 61 | "customAnswers": [ 62 | "Very perceptive" 63 | ] 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Add Task.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "f00a8363-d2a7-464c-a516-756bd0fd9579", 5 | "data": [ 6 | { 7 | "text": "remind me to " 8 | }, 9 | { 10 | "text": "mytask", 11 | "alias": "task_name", 12 | "meta": "@sys.any", 13 | "userDefined": true 14 | } 15 | ], 16 | "isTemplate": false, 17 | "count": 0 18 | }, 19 | { 20 | "id": "7c052d38-ea4b-4fea-81d9-c848ed9817e3", 21 | "data": [ 22 | { 23 | "text": "new task " 24 | }, 25 | { 26 | "text": "mytask", 27 | "alias": "task_name", 28 | "meta": "@sys.any", 29 | "userDefined": true 30 | } 31 | ], 32 | "isTemplate": false, 33 | "count": 0 34 | }, 35 | { 36 | "id": "f06eb6c2-f666-4ef4-b611-26ef40c07fad", 37 | "data": [ 38 | { 39 | "text": "add task " 40 | }, 41 | { 42 | "text": "mytask", 43 | "alias": "task_name", 44 | "meta": "@sys.any", 45 | "userDefined": true 46 | } 47 | ], 48 | "isTemplate": false, 49 | "count": 0 50 | } 51 | ], 52 | "id": "fd7540a4-6124-4261-8610-0cb58f53901a", 53 | "name": "Add Task", 54 | "auto": true, 55 | "contexts": [], 56 | "responses": [ 57 | { 58 | "resetContexts": false, 59 | "action": "input.task_add", 60 | "affectedContexts": [], 61 | "parameters": [ 62 | { 63 | "dataType": "@sys.any", 64 | "name": "task_name", 65 | "value": "$task_name", 66 | "isList": false 67 | } 68 | ], 69 | "messages": [ 70 | { 71 | "type": 0, 72 | "speech": [] 73 | } 74 | ] 75 | } 76 | ], 77 | "priority": 500000, 78 | "webhookUsed": true, 79 | "webhookForSlotFilling": false, 80 | "fallbackIntent": false, 81 | "events": [] 82 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Daily Journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "03eee004-1f1e-4b9d-af23-e18e866ca645", 5 | "data": [ 6 | { 7 | "text": "submit journal" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "3ce68ec6-6cc0-4277-aa09-c83ef418eb43", 15 | "data": [ 16 | { 17 | "text": "daily report" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "8e2ed722-761b-4903-bf7b-dd2b0ebdbad0", 25 | "data": [ 26 | { 27 | "text": "daily journal" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | } 33 | ], 34 | "id": "16a6da90-0673-47fe-a6f4-c33101493d7d", 35 | "name": "Daily Journal", 36 | "auto": true, 37 | "contexts": [], 38 | "responses": [ 39 | { 40 | "resetContexts": false, 41 | "action": "input.journal", 42 | "affectedContexts": [ 43 | { 44 | "name": "DailyJournal-followup", 45 | "parameters": {}, 46 | "lifespan": 2 47 | } 48 | ], 49 | "parameters": [], 50 | "messages": [ 51 | { 52 | "type": 0, 53 | "speech": [] 54 | } 55 | ] 56 | } 57 | ], 58 | "priority": 500000, 59 | "webhookUsed": true, 60 | "webhookForSlotFilling": false, 61 | "fallbackIntent": false, 62 | "events": [] 63 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Default Fallback Intent.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [], 3 | "id": "7476f8f9-309d-405d-8739-d2d374b4c585", 4 | "name": "Default Fallback Intent", 5 | "auto": false, 6 | "contexts": [], 7 | "responses": [ 8 | { 9 | "resetContexts": false, 10 | "action": "input.unknown", 11 | "affectedContexts": [], 12 | "parameters": [], 13 | "messages": [ 14 | { 15 | "type": 0, 16 | "speech": [ 17 | "I didn\u0027t get that. Can you say it again?", 18 | "I missed what you said. Say it again? Or try saying \"what can I do\"", 19 | "Sorry. Try saying \"what can I do\"", 20 | "Sorry, can you say that again? Or try saying \"Help me\"", 21 | "Say that again?" 22 | ] 23 | } 24 | ] 25 | } 26 | ], 27 | "priority": 500000, 28 | "webhookUsed": false, 29 | "webhookForSlotFilling": false, 30 | "fallbackIntent": true, 31 | "events": [] 32 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Disconnect.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "0ad7052b-5a5f-47b2-8764-82ce720d492b", 5 | "data": [ 6 | { 7 | "text": "Disconnect from Flow" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "09408b71-e794-47c0-9c56-1fd286b437d5", 15 | "data": [ 16 | { 17 | "text": "Disconnect" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | } 23 | ], 24 | "id": "e04b1e32-e8a3-4150-8e82-8b9c26e017e2", 25 | "name": "Disconnect", 26 | "auto": true, 27 | "contexts": [], 28 | "responses": [ 29 | { 30 | "resetContexts": false, 31 | "action": "input.disconnect", 32 | "affectedContexts": [], 33 | "parameters": [], 34 | "messages": [ 35 | { 36 | "type": 0, 37 | "speech": [] 38 | } 39 | ] 40 | } 41 | ], 42 | "priority": 500000, 43 | "webhookUsed": true, 44 | "webhookForSlotFilling": false, 45 | "fallbackIntent": false, 46 | "events": [] 47 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Flow Status Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "4826b06e-c011-46c6-96e3-abb7ea805be6", 5 | "data": [ 6 | { 7 | "text": "how\u0027s it going" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "900954aa-5e2c-41ff-ae98-da95ad5d4f3f", 15 | "data": [ 16 | { 17 | "text": "what\u0027s next" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "f0014521-c7fa-4053-bf6f-ecba14783cb7", 25 | "data": [ 26 | { 27 | "text": "status update" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | }, 33 | { 34 | "id": "70928d23-32a4-43fd-9663-b3e34c142d33", 35 | "data": [ 36 | { 37 | "text": "tell me about my day" 38 | } 39 | ], 40 | "isTemplate": false, 41 | "count": 0 42 | }, 43 | { 44 | "id": "4d5da642-6c14-428a-a85c-277b76ba9375", 45 | "data": [ 46 | { 47 | "text": "how am i doing?" 48 | } 49 | ], 50 | "isTemplate": false, 51 | "count": 0 52 | } 53 | ], 54 | "id": "308e5379-7d79-42dd-b66c-7c1d44e1c2fd", 55 | "name": "Flow Status Request", 56 | "auto": true, 57 | "contexts": [], 58 | "responses": [ 59 | { 60 | "resetContexts": false, 61 | "action": "input.status_request", 62 | "affectedContexts": [], 63 | "parameters": [], 64 | "messages": [ 65 | { 66 | "type": 0, 67 | "speech": "Sure, checking" 68 | } 69 | ] 70 | } 71 | ], 72 | "priority": 500000, 73 | "webhookUsed": true, 74 | "webhookForSlotFilling": false, 75 | "fallbackIntent": false, 76 | "events": [] 77 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Goals Help.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "a0c82d4d-11fd-4518-8984-e5d74b67be3d", 5 | "data": [ 6 | { 7 | "text": "how do goals work" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "03d71f70-12c1-41bb-bdac-c324952996dd", 15 | "data": [ 16 | { 17 | "text": "info on goals" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "08dd1a95-f655-4dcf-91f4-5b9beb41ba4d", 25 | "data": [ 26 | { 27 | "text": "goals?" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | }, 33 | { 34 | "id": "f209c7fb-63a5-4ab3-a80d-5a15af9c0340", 35 | "data": [ 36 | { 37 | "text": "tell me about goals" 38 | } 39 | ], 40 | "isTemplate": false, 41 | "count": 0 42 | }, 43 | { 44 | "id": "e5a1f590-8c41-4e5a-bd96-4df738b700f1", 45 | "data": [ 46 | { 47 | "text": "what are goals" 48 | } 49 | ], 50 | "isTemplate": false, 51 | "count": 0 52 | }, 53 | { 54 | "id": "772ea06f-c830-4f9c-993e-d1c4c22a04df", 55 | "data": [ 56 | { 57 | "text": "help on goals" 58 | } 59 | ], 60 | "isTemplate": false, 61 | "count": 0 62 | } 63 | ], 64 | "id": "094492de-3c05-4858-bcf7-d5b222dd5787", 65 | "name": "Goals Help", 66 | "auto": true, 67 | "contexts": [], 68 | "responses": [ 69 | { 70 | "resetContexts": false, 71 | "action": "input.help_goals", 72 | "affectedContexts": [], 73 | "parameters": [], 74 | "messages": [ 75 | { 76 | "type": 0, 77 | "speech": [] 78 | } 79 | ] 80 | } 81 | ], 82 | "priority": 500000, 83 | "webhookUsed": true, 84 | "webhookForSlotFilling": false, 85 | "fallbackIntent": false, 86 | "events": [] 87 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Goals Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "81a59864-d37b-4682-97f1-daad809b6578", 5 | "data": [ 6 | { 7 | "text": "This month", 8 | "meta": "@sys.ignore", 9 | "userDefined": false 10 | } 11 | ], 12 | "isTemplate": false, 13 | "count": 0 14 | }, 15 | { 16 | "id": "a075bd24-5396-4ba1-ac3c-49eed29e1f04", 17 | "data": [ 18 | { 19 | "text": "What do I want to do this month?" 20 | } 21 | ], 22 | "isTemplate": false, 23 | "count": 0 24 | }, 25 | { 26 | "id": "5c57f315-4a2e-4ca2-aae4-da99aa50dd39", 27 | "data": [ 28 | { 29 | "text": "What are my monthly goals?" 30 | } 31 | ], 32 | "isTemplate": false, 33 | "count": 0 34 | }, 35 | { 36 | "id": "57ff5d97-b6b7-4e52-a9d3-b8a9b34212c4", 37 | "data": [ 38 | { 39 | "text": "Tell me about my goals?" 40 | } 41 | ], 42 | "isTemplate": false, 43 | "count": 0 44 | }, 45 | { 46 | "id": "72082bd4-4dc3-4886-a088-ab47d016f5ee", 47 | "data": [ 48 | { 49 | "text": "What are my goals?" 50 | } 51 | ], 52 | "isTemplate": false, 53 | "count": 0 54 | } 55 | ], 56 | "id": "10f72820-b50b-44e5-bd4d-0db1e55d43a1", 57 | "name": "Goals Request", 58 | "auto": true, 59 | "contexts": [], 60 | "responses": [ 61 | { 62 | "resetContexts": false, 63 | "action": "input.goals_request", 64 | "affectedContexts": [], 65 | "parameters": [], 66 | "messages": [ 67 | { 68 | "type": 0, 69 | "speech": [ 70 | "Sure, let me check on that", 71 | "Sure, I\u0027ll get your goals" 72 | ] 73 | } 74 | ] 75 | } 76 | ], 77 | "priority": 500000, 78 | "webhookUsed": true, 79 | "webhookForSlotFilling": false, 80 | "fallbackIntent": false, 81 | "events": [] 82 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Habit Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "6e5ef50c-e193-425a-ad7c-19ec356d1adf", 5 | "data": [ 6 | { 7 | "text": "habit status" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "9c3d1a68-1ae3-4be8-951d-e2360ba8d6b8", 15 | "data": [ 16 | { 17 | "text": "tell me about my habits" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "025f51ab-7ca0-4ee3-a0fa-5a0517379df0", 25 | "data": [ 26 | { 27 | "text": "my habits" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | }, 33 | { 34 | "id": "943968ae-c933-43c7-9aec-291ff0044206", 35 | "data": [ 36 | { 37 | "text": "how am i doing on habits" 38 | } 39 | ], 40 | "isTemplate": false, 41 | "count": 0 42 | } 43 | ], 44 | "id": "d721fe0f-48cc-411d-8b6d-0b31c38e52c8", 45 | "name": "Habit Request", 46 | "auto": true, 47 | "contexts": [], 48 | "responses": [ 49 | { 50 | "resetContexts": false, 51 | "action": "input.habit_status", 52 | "affectedContexts": [], 53 | "parameters": [], 54 | "messages": [ 55 | { 56 | "type": 0, 57 | "speech": [] 58 | } 59 | ] 60 | } 61 | ], 62 | "priority": 500000, 63 | "webhookUsed": true, 64 | "webhookForSlotFilling": false, 65 | "fallbackIntent": false, 66 | "events": [] 67 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Habits Help.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "791d74ce-f0a9-4726-a31d-091b8a2081d7", 5 | "data": [ 6 | { 7 | "text": "What are habits" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "5b94d029-6b07-42c0-8131-0ce28b0ea089", 15 | "data": [ 16 | { 17 | "text": "Tell me about habits" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "c1a18cf8-1d00-4c90-b2b6-e6c35c68880c", 25 | "data": [ 26 | { 27 | "text": "How do habits work" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | }, 33 | { 34 | "id": "16c5f150-dc2c-4dbc-b283-7684985d921f", 35 | "data": [ 36 | { 37 | "text": "Help on habits" 38 | } 39 | ], 40 | "isTemplate": false, 41 | "count": 0 42 | } 43 | ], 44 | "id": "ec792743-3a65-4dc8-b378-c1a25e0f9b5f", 45 | "name": "Habits Help", 46 | "auto": true, 47 | "contexts": [], 48 | "responses": [ 49 | { 50 | "resetContexts": false, 51 | "action": "input.help_habits", 52 | "affectedContexts": [], 53 | "parameters": [], 54 | "messages": [ 55 | { 56 | "type": 0, 57 | "speech": [] 58 | } 59 | ] 60 | } 61 | ], 62 | "priority": 500000, 63 | "webhookUsed": true, 64 | "webhookForSlotFilling": false, 65 | "fallbackIntent": false, 66 | "events": [] 67 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Help.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "1ef02267-cd1d-4e98-ad63-8e07ba18d486", 5 | "data": [ 6 | { 7 | "text": "How does this work" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "05dfd6a5-ffb0-43d4-8d8f-f029e8fa7915", 15 | "data": [ 16 | { 17 | "text": "What else can I do" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "0706185c-e1cf-44ac-afc3-d5987e016811", 25 | "data": [ 26 | { 27 | "text": "What can I say" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | }, 33 | { 34 | "id": "1ea32308-201b-4840-b2b4-6633c0ca24d5", 35 | "data": [ 36 | { 37 | "text": "What can I do" 38 | } 39 | ], 40 | "isTemplate": false, 41 | "count": 0 42 | }, 43 | { 44 | "id": "c70b1d72-fe13-44d6-b7dc-db4008a8b5b1", 45 | "data": [ 46 | { 47 | "text": "Help me" 48 | } 49 | ], 50 | "isTemplate": false, 51 | "count": 0 52 | } 53 | ], 54 | "id": "8633d4e9-a3b2-49ba-bd78-d781ca04b280", 55 | "name": "Help", 56 | "auto": true, 57 | "contexts": [], 58 | "responses": [ 59 | { 60 | "resetContexts": false, 61 | "action": "input.help", 62 | "affectedContexts": [], 63 | "parameters": [], 64 | "messages": [ 65 | { 66 | "type": 0, 67 | "speech": [ 68 | "One thing you can do is report habits completed, for example: Mark \u0027run\u0027 as completed.", 69 | "One thing you can do is check on your goals, say: \"What are my goals?\"", 70 | "You can commit to habits, for example: \"I\u0027m going to \u0027run\u0027 today\" or \"Commit to \u0027run\u0027 today\"", 71 | "One thing you can do is check your overall status. Try saying: \"How am I doing?\"" 72 | ] 73 | } 74 | ] 75 | } 76 | ], 77 | "priority": 500000, 78 | "webhookUsed": false, 79 | "webhookForSlotFilling": false, 80 | "fallbackIntent": false, 81 | "events": [] 82 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Report Habit Commitment.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "9a3da1bc-1fd0-4062-9cb9-d216219f94dd", 5 | "data": [ 6 | { 7 | "text": "commit to " 8 | }, 9 | { 10 | "text": "habit", 11 | "alias": "habit", 12 | "meta": "@sys.any", 13 | "userDefined": true 14 | }, 15 | { 16 | "text": " " 17 | }, 18 | { 19 | "text": "later", 20 | "alias": "time", 21 | "meta": "@sys.time", 22 | "userDefined": true 23 | } 24 | ], 25 | "isTemplate": false, 26 | "count": 0 27 | }, 28 | { 29 | "id": "08a8db1c-f346-435b-9770-241987ff0f52", 30 | "data": [ 31 | { 32 | "text": "Make sure I " 33 | }, 34 | { 35 | "text": "habit", 36 | "alias": "habit", 37 | "meta": "@sys.any", 38 | "userDefined": true 39 | }, 40 | { 41 | "text": " today" 42 | } 43 | ], 44 | "isTemplate": false, 45 | "count": 0 46 | }, 47 | { 48 | "id": "7085f475-b226-4aee-9cf4-8f6bd12d9b21", 49 | "data": [ 50 | { 51 | "text": "I\u0027m going to " 52 | }, 53 | { 54 | "text": "habit", 55 | "alias": "habit", 56 | "meta": "@sys.any", 57 | "userDefined": true 58 | }, 59 | { 60 | "text": " today" 61 | } 62 | ], 63 | "isTemplate": false, 64 | "count": 0 65 | }, 66 | { 67 | "id": "91c5ed4e-875f-4aba-8b6a-1b0559f17e2a", 68 | "data": [ 69 | { 70 | "text": "Commit to " 71 | }, 72 | { 73 | "text": "habit", 74 | "alias": "habit", 75 | "meta": "@sys.any", 76 | "userDefined": true 77 | }, 78 | { 79 | "text": " " 80 | }, 81 | { 82 | "text": "today", 83 | "meta": "@sys.ignore", 84 | "userDefined": false 85 | } 86 | ], 87 | "isTemplate": false, 88 | "count": 0 89 | } 90 | ], 91 | "id": "b2019cfc-f53b-4c36-80a0-32ec1c742f22", 92 | "name": "Report Habit Commitment", 93 | "auto": true, 94 | "contexts": [], 95 | "responses": [ 96 | { 97 | "resetContexts": false, 98 | "action": "input.habit_commit", 99 | "affectedContexts": [], 100 | "parameters": [ 101 | { 102 | "required": true, 103 | "dataType": "@sys.any", 104 | "name": "habit", 105 | "value": "$habit", 106 | "prompts": [ 107 | "Which habit?" 108 | ] 109 | }, 110 | { 111 | "dataType": "@sys.time", 112 | "name": "time", 113 | "value": "$time", 114 | "isList": false 115 | } 116 | ], 117 | "messages": [ 118 | { 119 | "type": 0, 120 | "speech": [ 121 | "On it", 122 | "OK" 123 | ] 124 | } 125 | ] 126 | } 127 | ], 128 | "priority": 500000, 129 | "webhookUsed": true, 130 | "webhookForSlotFilling": false, 131 | "fallbackIntent": false, 132 | "events": [] 133 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Task Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "7a300ed0-cabc-4662-9609-496f76af829a", 5 | "data": [ 6 | { 7 | "text": "tasks remaining" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "58f02fde-7f41-4b07-bdd8-f5acf63695e4", 15 | "data": [ 16 | { 17 | "text": "what do i need to do" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "17093932-b802-42e5-a46c-4305c1901fc9", 25 | "data": [ 26 | { 27 | "text": "tasks " 28 | }, 29 | { 30 | "text": "today", 31 | "meta": "@sys.ignore", 32 | "userDefined": false 33 | } 34 | ], 35 | "isTemplate": false, 36 | "count": 0 37 | }, 38 | { 39 | "id": "2ba9310f-af0e-4521-ba37-ddf8dcbcfb6b", 40 | "data": [ 41 | { 42 | "text": "today", 43 | "meta": "@sys.ignore", 44 | "userDefined": false 45 | }, 46 | { 47 | "text": "\u0027s tasks" 48 | } 49 | ], 50 | "isTemplate": false, 51 | "count": 0 52 | }, 53 | { 54 | "id": "50d84999-36b7-4334-9dba-78ee78bab024", 55 | "data": [ 56 | { 57 | "text": "what are my tasks" 58 | } 59 | ], 60 | "isTemplate": false, 61 | "count": 0 62 | }, 63 | { 64 | "id": "fc80c153-8ece-459c-b7d8-9f59e8b252b6", 65 | "data": [ 66 | { 67 | "text": "my tasks" 68 | } 69 | ], 70 | "isTemplate": false, 71 | "count": 0 72 | } 73 | ], 74 | "id": "6d145e1f-da03-4a9d-b070-ffec1ed27f51", 75 | "name": "Task Request", 76 | "auto": true, 77 | "contexts": [], 78 | "responses": [ 79 | { 80 | "resetContexts": false, 81 | "action": "input.task_view", 82 | "affectedContexts": [], 83 | "parameters": [], 84 | "messages": [ 85 | { 86 | "type": 0, 87 | "speech": [] 88 | } 89 | ] 90 | } 91 | ], 92 | "priority": 500000, 93 | "webhookUsed": true, 94 | "webhookForSlotFilling": false, 95 | "fallbackIntent": false, 96 | "events": [] 97 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Tasks Help.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "9c36423f-b90e-4c71-8c15-89d5822fc03d", 5 | "data": [ 6 | { 7 | "text": "How do tasks work" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "1f1c6e7c-21ce-4906-96c1-327d2241f36e", 15 | "data": [ 16 | { 17 | "text": "Tell me about tasks" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "0b82843e-b4b3-4c59-bc93-b04c605da8fa", 25 | "data": [ 26 | { 27 | "text": "What are tasks" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | }, 33 | { 34 | "id": "5399fefd-13f9-48fc-970e-aa645142db3d", 35 | "data": [ 36 | { 37 | "text": "Help on tasks" 38 | } 39 | ], 40 | "isTemplate": false, 41 | "count": 0 42 | } 43 | ], 44 | "id": "9b9973d1-d279-4b14-962a-729e2973ee9f", 45 | "name": "Tasks Help", 46 | "auto": true, 47 | "contexts": [], 48 | "responses": [ 49 | { 50 | "resetContexts": false, 51 | "action": "input.help_tasks", 52 | "affectedContexts": [], 53 | "parameters": [], 54 | "messages": [ 55 | { 56 | "type": 0, 57 | "speech": [] 58 | } 59 | ] 60 | } 61 | ], 62 | "priority": 500000, 63 | "webhookUsed": true, 64 | "webhookForSlotFilling": false, 65 | "fallbackIntent": false, 66 | "events": [] 67 | } -------------------------------------------------------------------------------- /static/flow-agent/intents/Welcome.json: -------------------------------------------------------------------------------- 1 | { 2 | "userSays": [ 3 | { 4 | "id": "93e0d482-1cfa-4771-b861-13532fb1c4d6", 5 | "data": [ 6 | { 7 | "text": "How does Flow work?" 8 | } 9 | ], 10 | "isTemplate": false, 11 | "count": 0 12 | }, 13 | { 14 | "id": "d9eacef2-28aa-4cfe-9cb1-f87512972e37", 15 | "data": [ 16 | { 17 | "text": "How does this work?" 18 | } 19 | ], 20 | "isTemplate": false, 21 | "count": 0 22 | }, 23 | { 24 | "id": "15d871d6-6699-4edd-9c6a-6add5ea04ee2", 25 | "data": [ 26 | { 27 | "text": "What\u0027s Flow?" 28 | } 29 | ], 30 | "isTemplate": false, 31 | "count": 0 32 | } 33 | ], 34 | "id": "f9dfbe01-3057-4c6c-9ad5-3067f625116e", 35 | "name": "Welcome", 36 | "auto": true, 37 | "contexts": [], 38 | "responses": [ 39 | { 40 | "resetContexts": false, 41 | "action": "input.welcome", 42 | "affectedContexts": [], 43 | "parameters": [], 44 | "messages": [ 45 | { 46 | "type": 0, 47 | "speech": [ 48 | "Flow here. Try saying \"how am I doing\", \"what are my goals\", or \"how do habits work\".", 49 | "Flow here. Try saying \"my status\", \"my tasks\", or \"how do goals work\".", 50 | "Hello, what can I do for you?" 51 | ] 52 | } 53 | ] 54 | } 55 | ], 56 | "priority": 500000, 57 | "webhookUsed": false, 58 | "webhookForSlotFilling": false, 59 | "fallbackIntent": false, 60 | "events": [ 61 | { 62 | "name": "GOOGLE_ASSISTANT_WELCOME" 63 | } 64 | ] 65 | } -------------------------------------------------------------------------------- /static/icons.css: -------------------------------------------------------------------------------- 1 | 2 | .icons.transp { 3 | opacity: 0.5; 4 | transition: all .2s ease; 5 | } 6 | 7 | .icons.round { 8 | border-radius: 2px; 9 | -o-border-radius: 2px; 10 | -moz-border-radius: 2px; 11 | } 12 | 13 | .icons.transp:hover { 14 | opacity: 1; 15 | } 16 | 17 | .icons { 18 | display: inline-block; 19 | padding: 0; 20 | vertical-align: middle; 21 | background-repeat: no-repeat; 22 | } 23 | 24 | .icons.right { 25 | float: right; 26 | } 27 | 28 | .icons._100 { 29 | width: 100px; 30 | height: 100px; 31 | background-image: url(/images/icons/icons_100.png); 32 | } 33 | 34 | .icons._50 { 35 | width: 50px; 36 | height: 50px; 37 | background-image: url(/images/icons/icons_50.png); 38 | } 39 | 40 | a:hover > .icons._100:not(.noroll) { background-position-y: -100px; } 41 | a:hover > .icons._50:not(.noroll) { background-position-y: -50px; } 42 | 43 | .icons._100.github { background-position: 0 0; } 44 | .icons._100.linkedin { background-position: -100px 0; } 45 | .icons._100.twitter { background-position: -200px 0; } 46 | .icons._100.google { background-position: -300px 0; } 47 | .icons._100.facebook { background-position: -400px 0; } 48 | .icons._100.flashcast { background-position: -500px 0; } 49 | .icons._100.echo { background-position: -600px 0; } 50 | 51 | .icons._50.github { background-position: 0 0; } 52 | .icons._50.linkedin { background-position: -50px 0; } 53 | .icons._50.twitter { background-position: -100px 0; } 54 | .icons._50.google { background-position: -150px 0; } 55 | .icons._50.facebook { background-position: -200px 0; } 56 | .icons._50.instagram { background-position: -250px 0; } 57 | .icons._50.echo { background-position: -300px 0; } 58 | .icons._50.pin { background-position: -350px 0; } 59 | .icons._50.back { background-position: -400px 0; } 60 | .icons._50.medium { background-position: -450px 0; } 61 | .icons._50.vc4a { background-position: -500px 0; } 62 | .icons._50.link { background-position: -550px 0; } 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Flow", 3 | "name": "Flow", 4 | "description": "A personal dashboard to focus on what matters", 5 | "theme_color":"#333333", 6 | "background_color":"#000000", 7 | "icons": [ 8 | { 9 | "src": "favicon-16x16.png", 10 | "type": "image/png", 11 | "sizes": "16x16" 12 | }, 13 | { 14 | "src": "favicon-32x32.png", 15 | "type": "image/png", 16 | "sizes": "32x32" 17 | }, 18 | { 19 | "src": "favicon-96x96.png", 20 | "type": "image/png", 21 | "sizes": "96x96" 22 | } 23 | ], 24 | "start_url": "/app/dashboard", 25 | "display":"standalone" 26 | } 27 | -------------------------------------------------------------------------------- /static/sounds/beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/sounds/beep.mp3 -------------------------------------------------------------------------------- /static/sounds/commit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/sounds/commit.mp3 -------------------------------------------------------------------------------- /static/sounds/complete.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/static/sounds/complete.mp3 -------------------------------------------------------------------------------- /swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | description: | 4 | Flow Dashboard APIs 5 | [http://flowdash.co](http://flowdash.co) 6 | version: "1.2" 7 | title: Flow Dashboard 8 | contact: 9 | name: Jeremy Gordon 10 | url: http://jgordon.io 11 | email: onejgordon@gmail.com 12 | license: 13 | name: MIT 14 | url: https://jeremy.mit-license.org/ 15 | host: flowdash.co 16 | basePath: /api 17 | schemes: 18 | - https 19 | securityDefinitions: 20 | basicAuth: 21 | description: base64 encode user_email:user_api_password 22 | type: basic 23 | security: 24 | - basicAuth: [] 25 | paths: 26 | /tracking: 27 | post: 28 | summary: Submit arbitrary tracking data for a particular date 29 | operationId: submitTracking 30 | consumes: 31 | - application/x-www-form-urlencoded 32 | produces: 33 | - application/json 34 | parameters: 35 | - in: formData 36 | name: date 37 | description: Date (YYYY-MM-DD) 38 | type: string 39 | - in: formData 40 | name: data 41 | description: Stringified simple JSON object (keys and values as strings) Will be merged if any existing data has been posted. 42 | type: string 43 | responses: 44 | 200: 45 | description: successful update 46 | schema: 47 | type: object 48 | properties: 49 | success: 50 | type: boolean 51 | example: true 52 | message: 53 | type: string 54 | example: "OK" 55 | tracking_day: 56 | $ref: '#/definitions/tracking_day' 57 | /snapshot: 58 | post: 59 | summary: Submit a snapshot 60 | operationId: submitSnapshot 61 | consumes: 62 | - application/x-www-form-urlencoded 63 | produces: 64 | - application/json 65 | parameters: 66 | - in: formData 67 | name: lat 68 | description: Latitude 69 | type: string 70 | - in: formData 71 | name: lon 72 | description: Latitude 73 | type: string 74 | - in: formData 75 | name: place 76 | description: Place 77 | type: string 78 | required: true 79 | - in: formData 80 | name: activity 81 | description: Activity 82 | type: string 83 | required: true 84 | - in: formData 85 | name: people 86 | description: People (comma separated) 87 | type: array 88 | collectionFormat: csv 89 | items: 90 | type: string 91 | required: true 92 | - in: formData 93 | name: metrics 94 | description: Stringified JSON object of metrics (keys - metric names, values - integer) 95 | type: string 96 | required: true 97 | responses: 98 | 200: 99 | description: successful submission 100 | schema: 101 | type: object 102 | properties: 103 | success: 104 | type: boolean 105 | example: true 106 | message: 107 | type: string 108 | example: "OK" 109 | snapshot: 110 | $ref: '#/definitions/snapshot' 111 | definitions: 112 | snapshot: 113 | type: object 114 | properties: 115 | id: 116 | type: integer 117 | ts: 118 | type: integer 119 | iso_date: 120 | type: string 121 | pattern: '^(\d\d\d\d\-\d\d\-\d\d)$' 122 | people: 123 | type: array 124 | place: 125 | type: string 126 | activity: 127 | type: string 128 | lat: 129 | type: string 130 | lon: 131 | type: string 132 | tracking_day: 133 | type: object 134 | properties: 135 | id: 136 | type: integer 137 | iso_date: 138 | type: string 139 | pattern: '^(\d\d\d\d\-\d\d\-\d\d)$' 140 | data: 141 | type: object 142 | -------------------------------------------------------------------------------- /testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/testing/__init__.py -------------------------------------------------------------------------------- /testing/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | Execute ./run_tests.sh to run all tests in this folder -------------------------------------------------------------------------------- /testing/testing_authentication.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from google.appengine.ext import db 5 | from google.appengine.ext import testbed 6 | from datetime import datetime 7 | from models import User 8 | from base_test_case import BaseTestCase 9 | from flow import app as tst_app 10 | import tools 11 | import imp 12 | try: 13 | imp.find_module('secrets', ['settings']) 14 | except ImportError: 15 | from settings import secrets_template as secrets 16 | else: 17 | from settings import secrets 18 | 19 | USER_GOOGLE_ID = "1234" 20 | 21 | 22 | class AuthenticationTestCase(BaseTestCase): 23 | 24 | def setUp(self): 25 | self.set_application(tst_app) 26 | self.setup_testbed() 27 | self.init_datastore_stub() 28 | self.init_memcache_stub() 29 | self.init_taskqueue_stub() 30 | self.register_search_api_stub() 31 | self.init_mail_stub() 32 | 33 | def testUserAccessEncodeDecode(self): 34 | user = User.Create(email="test@example.com") 35 | user.put() 36 | access_token = user.aes_access_token(client_id="test") 37 | user_id = User.user_id_from_aes_access_token(access_token) 38 | self.assertIsNotNone(access_token) 39 | self.assertEqual(user_id, user.key.id()) 40 | 41 | def testUserGoogleSimpleAccountLinking(self): 42 | import jwt 43 | user = User.Create(email="test@example.com", g_id=USER_GOOGLE_ID) 44 | user.put() 45 | 46 | creation = int(tools.unixtime(ms=False)) 47 | payload = { 48 | 'iss': 'https://accounts.google.com', 49 | 'aud': secrets.GOOGLE_CLIENT_ID, 50 | 'sub': USER_GOOGLE_ID, 51 | 'email': "test@example.com", 52 | 'locale': "en_US", 53 | "iat": creation, 54 | "exp": creation + 60*60 55 | } 56 | params = { 57 | 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 58 | 'intent': 'get', 59 | 'assertion': jwt.encode(payload, secrets.GOOGLE_CLIENT_SECRET, algorithm='HS256') 60 | } 61 | response = self.post_json("/api/auth/google/token", params) 62 | token_type = response.get('token_type') 63 | self.assertEqual(token_type, 'bearer') 64 | 65 | -------------------------------------------------------------------------------- /testing/testing_facebook_requests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from datetime import datetime, timedelta 5 | from base_test_case import BaseTestCase 6 | from models import Goal 7 | from flow import app as tst_app 8 | from models import Habit, Task 9 | from services.agent import FacebookAgent 10 | import json 11 | 12 | 13 | class DummyRequest(): 14 | 15 | def __init__(self, body): 16 | self.body = json.dumps(body) 17 | 18 | FB_ID = "1182039228580000" 19 | 20 | class FacebookTestCase(BaseTestCase): 21 | 22 | def setUp(self): 23 | self.set_application(tst_app) 24 | self.setup_testbed() 25 | self.init_datastore_stub() 26 | self.init_memcache_stub() 27 | self.init_taskqueue_stub() 28 | self.init_mail_stub() 29 | self.register_search_api_stub() 30 | self.init_app_basics() 31 | 32 | self.u = u = self.users[0] 33 | self.u.Update(name="George", fb_id=FB_ID) 34 | self.u.put() 35 | h = Habit.Create(u) 36 | h.Update(name="Run") 37 | h.put() 38 | t = Task.Create(u, "Dont forget the milk") 39 | t.put() 40 | g = Goal.CreateMonthly(u, date=datetime.today().date()) 41 | g.Update(text=["Get it done", "Also get exercise"]) 42 | g.put() 43 | 44 | 45 | def test_a_request(self): 46 | fa = FacebookAgent(DummyRequest({ 47 | 'entry': [ 48 | {'messaging': [ 49 | { 50 | 'timestamp': 1489442604947, 51 | 'message': {'text': 'how do goals work', 'mid': 'mid.123:e9c21f9b61', 'seq': 5445}, 52 | 'recipient': {'id': '197271657425000'}, 53 | 'sender': {'id': FB_ID} 54 | } 55 | ], 'id': '197271657425620', 'time': 1489442605073} 56 | ], 'object': 'page'} 57 | )) 58 | res_body = fa.send_response() 59 | self.assertTrue("You can review your monthly and annual goals. Try saying 'view goals'" in res_body.get('message', {}).get('text')) 60 | 61 | 62 | def test_account_linking_request(self): 63 | fa = FacebookAgent(DummyRequest({ 64 | 'entry': [ 65 | {'messaging': [ 66 | { 67 | 'timestamp': 1489442604947, 68 | 'account_linking': { 69 | 'status': 'linked', 70 | 'authorization_code': self.u.key.id() 71 | }, 72 | 'recipient': {'id': '197271657425000'}, 73 | 'sender': {'id': FB_ID} 74 | } 75 | ], 'id': '197271657425620', 'time': 1489442605073} 76 | ], 'object': 'page'} 77 | )) 78 | res_body = fa.send_response() 79 | self.assertTrue("you've successfully connected with Flow!" in res_body.get('message', {}).get('text')) 80 | 81 | -------------------------------------------------------------------------------- /testing/testing_goals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from datetime import datetime 5 | from base_test_case import BaseTestCase 6 | from models import Goal, User 7 | from flow import app as tst_app 8 | 9 | 10 | class GoalsTestCase(BaseTestCase): 11 | 12 | def setUp(self): 13 | self.set_application(tst_app) 14 | self.setup_testbed() 15 | self.init_standard_stubs() 16 | self.init_app_basics() 17 | u = self.users[0] 18 | self.goal_annual = Goal.Create(u, '2017') 19 | self.goal_annual.Update(text=["Annual goal 1", "Annual goal 2"]) 20 | self.goal_monthly = Goal.CreateMonthly(u) 21 | self.goal_monthly.put() 22 | self.goal_annual.put() 23 | 24 | def test_types(self): 25 | self.assertTrue(self.goal_monthly.monthly()) 26 | self.assertTrue(self.goal_annual.annual()) 27 | -------------------------------------------------------------------------------- /testing/testing_habits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from datetime import datetime, timedelta 5 | from base_test_case import BaseTestCase 6 | from models import Habit, HabitDay 7 | from flow import app as tst_app 8 | 9 | 10 | class HabitTestCase(BaseTestCase): 11 | 12 | def setUp(self): 13 | self.set_application(tst_app) 14 | self.setup_testbed() 15 | self.init_datastore_stub() 16 | self.init_memcache_stub() 17 | self.init_taskqueue_stub() 18 | self.init_mail_stub() 19 | self.init_app_basics() 20 | self.register_search_api_stub() 21 | 22 | u = self.users[0] 23 | habit_run = Habit.Create(u) 24 | habit_run.Update(name="Run") 25 | habit_run.put() 26 | self.habit_run = habit_run 27 | 28 | habit_read = Habit.Create(u) 29 | habit_read.Update(name="Read") 30 | habit_read.put() 31 | self.habit_read = habit_read 32 | 33 | def test_toggle(self): 34 | # Mark done (creating new habit day) 35 | marked_done, hd = HabitDay.Toggle(self.habit_run, datetime.today()) 36 | self.assertTrue(marked_done) 37 | self.assertIsNotNone(hd) 38 | self.assertTrue(hd.done) 39 | 40 | # Mark not done 41 | marked_done, hd = HabitDay.Toggle(self.habit_run, datetime.today()) 42 | self.assertFalse(marked_done) 43 | self.assertIsNotNone(hd) 44 | self.assertFalse(hd.done) 45 | 46 | def test_retrieve_history(self): 47 | # Toggle today and yesterday (create 2 habitdays) 48 | marked_done, hd = HabitDay.Toggle(self.habit_read, datetime.today()) 49 | marked_done, hd = HabitDay.Toggle(self.habit_read, datetime.today() - timedelta(days=1)) 50 | hd_keys = HabitDay.All(self.habit_read.key) 51 | self.assertEqual(len(hd_keys), 2) 52 | 53 | def test_delete_history(self): 54 | marked_done, hd = HabitDay.Toggle(self.habit_read, datetime.today()) 55 | marked_done, hd = HabitDay.Toggle(self.habit_run, datetime.today()) 56 | 57 | self.habit_read.delete_history() # Schedules background task 58 | self.execute_tasks_until_empty() 59 | hd_keys = HabitDay.All(self.habit_read.key) 60 | self.assertEqual(len(hd_keys), 0) # Confirm both deleted 61 | 62 | # Confirm habit_run not affected 63 | hd_keys = HabitDay.All(self.habit_run.key) 64 | self.assertEqual(len(hd_keys), 1) # Confirm still in db 65 | 66 | -------------------------------------------------------------------------------- /testing/testing_journaling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from google.appengine.api import memcache 5 | from google.appengine.ext import db 6 | from google.appengine.ext import testbed 7 | from datetime import datetime, timedelta 8 | from google.appengine.ext import deferred 9 | from base_test_case import BaseTestCase 10 | from models import JournalTag, MiniJournal, User 11 | from flow import app as tst_app 12 | 13 | 14 | class JournalingTestCase(BaseTestCase): 15 | 16 | def setUp(self): 17 | self.set_application(tst_app) 18 | self.setup_testbed() 19 | self.init_datastore_stub() 20 | self.init_memcache_stub() 21 | self.init_taskqueue_stub() 22 | self.init_mail_stub() 23 | self.register_search_api_stub() 24 | 25 | u = User.Create(email="test@example.com") 26 | u.put() 27 | self.u = u 28 | 29 | def test_journal_tag_parsign(self): 30 | volley = [ 31 | ("Fun #PoolParty with @KatyRoth", ["#PoolParty"], ["@KatyRoth"]), 32 | ("Stressful day at work with @BarackObama", [], ["@BarackObama"]), 33 | ("Went #Fishing with @JohnKariuki and got #Sick off #Seafood", ["#Fishing", "#Sick", "#Seafood"], ["@JohnKariuki"]), 34 | ("Went #Fishing with @BarackObama", ["#Fishing"], ["@BarackObama"]), 35 | (None, [], []), 36 | (5, [], []) 37 | ] 38 | for v in volley: 39 | txt, expected_hashes, expected_people = v 40 | jts = JournalTag.CreateFromText(self.u, txt) 41 | hashes = map(lambda jt: jt.key.id(), filter(lambda jt: not jt.person(), jts)) 42 | people = map(lambda jt: jt.key.id(), filter(lambda jt: jt.person(), jts)) 43 | self.assertEqual(expected_hashes, hashes) 44 | self.assertEqual(expected_people, people) 45 | 46 | self.assertEqual(len(JournalTag.All(self.u)), 7) 47 | 48 | 49 | -------------------------------------------------------------------------------- /testing/testing_projects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from datetime import datetime 5 | from base_test_case import BaseTestCase 6 | from models import Project, User 7 | from flow import app as tst_app 8 | 9 | 10 | class ProjectsTestCase(BaseTestCase): 11 | 12 | def setUp(self): 13 | self.set_application(tst_app) 14 | self.setup_testbed() 15 | self.init_datastore_stub() 16 | self.init_memcache_stub() 17 | self.init_taskqueue_stub() 18 | self.init_mail_stub() 19 | self.register_search_api_stub() 20 | 21 | u = User.Create(email="test@example.com") 22 | u.put() 23 | 24 | self.project = Project.Create(u) 25 | self.project.Update(title="Build App", subhead="Subhead", urls=["http://www.example.com"]) 26 | self.project.put() 27 | 28 | def test_setting_progress(self): 29 | set_progresses = [4, 6, 10, 8, 10] 30 | for p in set_progresses: 31 | regression = p < self.project.progress 32 | self.project.set_progress(p) 33 | if regression: 34 | # Expect timestamps after new progress to be cleared 35 | cleared_timestamps = self.project.progress_ts[(p - 10):] 36 | self.assertEqual(sum(cleared_timestamps), 0) 37 | self.project.put() 38 | 39 | progress_ts = self.project.progress_ts 40 | for p in set_progresses: 41 | self.assertTrue(progress_ts[p-1] > 0) 42 | self.assertTrue(self.project.is_completed()) 43 | self.assertIsNotNone(self.project.dt_completed) 44 | -------------------------------------------------------------------------------- /testing/testing_snapshots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from datetime import datetime 5 | from base_test_case import BaseTestCase 6 | from models import Snapshot 7 | from flow import app as tst_app 8 | 9 | 10 | class SnapshotTestCase(BaseTestCase): 11 | 12 | def setUp(self): 13 | self.set_application(tst_app) 14 | self.setup_testbed() 15 | self.init_standard_stubs() 16 | self.init_app_basics() 17 | 18 | self.u = self.users[0] 19 | 20 | def test_submit(self): 21 | volley = [ 22 | ("Working - Coding", "Office", {'happiness': 10, 'stress': 2}, {'activity': 'Working', 'activity_sub': "Coding", 'place': "Office"}), 23 | ("Working: Meeting", "Office", {'happiness': 2, 'stress': 4}, {'activity': 'Working', 'activity_sub': "Meeting", 'place': "Office"}), 24 | ("Running", "Track", {'happiness': 10, 'stress': 1}, {'activity': 'Running', 'activity_sub': None, 'place': "Track"}) 25 | ] 26 | for v in volley: 27 | activity, place, metrics, expected_vals = v 28 | kwargs = { 29 | 'activity': activity, 30 | 'place': place, 31 | 'metrics': metrics 32 | } 33 | sn = Snapshot.Create(self.u, **kwargs) 34 | sn.put() 35 | for key, val in expected_vals.items(): 36 | self.assertEqual(getattr(sn, key), val) 37 | for metric, val in metrics.items(): 38 | self.assertEqual(sn.get_data_value(metric), val) 39 | 40 | -------------------------------------------------------------------------------- /testing/testing_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf8 -*- 3 | 4 | from datetime import datetime 5 | from base_test_case import BaseTestCase 6 | from models import Project, User 7 | from flow import app as tst_app 8 | from datetime import timedelta 9 | import json 10 | 11 | 12 | class UsersTestCase(BaseTestCase): 13 | 14 | def setUp(self): 15 | self.set_application(tst_app) 16 | self.setup_testbed() 17 | self.init_datastore_stub() 18 | self.init_memcache_stub() 19 | self.init_taskqueue_stub() 20 | self.init_mail_stub() 21 | self.register_search_api_stub() 22 | self.init_app_basics() 23 | 24 | def test_local_time(self): 25 | u = self.users[0] 26 | u.timezone = "Africa/Nairobi" 27 | u.put() 28 | 29 | utc_now = datetime.now() 30 | self.assertEqual(u.local_time().hour, (utc_now + timedelta(hours=3)).hour) 31 | 32 | def test_levels(self): 33 | u = self.users[0] 34 | self.assertFalse(u.admin()) 35 | 36 | def test_password(self): 37 | u = self.users[0] 38 | pw = u.setPass() 39 | self.assertEqual(len(pw), 6) 40 | self.assertTrue(u.checkPass(pw)) 41 | 42 | def test_integration_props(self): 43 | u = self.users[0] 44 | u.set_integration_prop('key', 'value') 45 | integrations_dict = json.loads(u.integrations) 46 | self.assertEqual(integrations_dict.get('key'), 'value') 47 | 48 | -------------------------------------------------------------------------------- /views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onejgordon/flow-dashboard/b8d85d9313e51cf386f6d2e5944fc958a7d96769/views/__init__.py -------------------------------------------------------------------------------- /views/views.py: -------------------------------------------------------------------------------- 1 | import django_version 2 | import authorized 3 | import handlers 4 | import tools 5 | 6 | 7 | class App(handlers.BaseRequestHandler): 8 | @authorized.role() 9 | def get(self, *args, **kwargs): 10 | from settings.secrets import G_MAPS_API_KEY 11 | # gmods = { 12 | # "modules": [ 13 | # ] 14 | # } 15 | d = kwargs.get('d') 16 | d['constants'] = { 17 | 'dev': tools.on_dev_server() 18 | } 19 | d['alt_bootstrap'] = { 20 | "UserStore": { 21 | 'user': self.user.json(is_self=True) if self.user else None 22 | } 23 | } 24 | # d['gautoload'] = urllib.quote_plus(json.dumps(gmods).replace(' ','')) 25 | d['gmap_api_key'] = G_MAPS_API_KEY 26 | self.render_template("index.html", **d) 27 | 28 | --------------------------------------------------------------------------------