(.*)<\/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 |
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 |
42 |
43 |
44 |
45 |
46 |
47 |
53 |
54 |
57 |
58 |
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 | Bar Height
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | );
133 | }
134 | }
135 |
136 | module.exports = AnalysisTasks;
--------------------------------------------------------------------------------
/src/js/components/common/AsyncActionButton.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | import { RaisedButton, FlatButton } from 'material-ui';
3 |
4 | export default class AsyncActionButton extends React.Component {
5 | static defaultProps = {
6 | working: false,
7 | enabled: false,
8 | onClick: null,
9 | text_disabled: "Saved",
10 | text_working: "Saving...",
11 | text_enabled: "Save",
12 | raised: true,
13 | fullWidth: false
14 | }
15 |
16 | getMessage() {
17 | if (this.props.working) return this.props.text_working;
18 | else if (this.props.enabled) return this.props.text_enabled;
19 | else return this.props.text_disabled;
20 | }
21 |
22 | render() {
23 | let {working, enabled, raised, fullWidth} = this.props;
24 | let icon;
25 | var message = this.getMessage();
26 | if (working) icon =
27 | if (raised) return
33 | else return
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/js/components/common/BigProp.js:
--------------------------------------------------------------------------------
1 | var PropTypes = require('prop-types');
2 | var React = require('react');
3 |
4 | export default class BigProp extends React.Component {
5 | static propTypes = {
6 | label: PropTypes.string,
7 | value: PropTypes.node,
8 | color: PropTypes.string,
9 | size: PropTypes.string,
10 | icon: PropTypes.element,
11 | onClick: PropTypes.func,
12 | labelPosition: PropTypes.string,
13 | }
14 | static defaultProps = {
15 | label: '--',
16 | value: '--',
17 | color: 'white',
18 | icon: null,
19 | onClick: null,
20 | size: '1.7em',
21 | labelPosition: "bottom"
22 | }
23 |
24 | constructor(props) {
25 | super(props);
26 | this.state = {
27 | };
28 | }
29 |
30 | render() {
31 | let {label, value, color, size, icon, onClick, labelPosition} = this.props;
32 | let clickable = onClick != null;
33 | let boundClick = clickable ? onClick.bind(this) : null;
34 | let cls = "text-center bigProp";
35 | if (clickable) cls += " clickable";
36 | let order = [
37 | {icon}{value}
,
38 | {label}
39 | ];
40 | if (labelPosition == 'top') order = order.reverse();
41 | return (
42 |
43 | { order }
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/js/components/common/DateTime.js:
--------------------------------------------------------------------------------
1 | var PropTypes = require('prop-types');
2 | var React = require('react');
3 | var util = require('utils/util');
4 |
5 | export default class DateTime extends React.Component {
6 | static propTypes = {
7 | ms: PropTypes.number,
8 | prefix: PropTypes.string,
9 | color: PropTypes.string
10 | }
11 |
12 | static defaultProps = {
13 | ms: 0,
14 | prefix: null,
15 | color: null
16 | }
17 |
18 | render() {
19 | let {prefix, ms, color} = this.props;
20 | let {very_old, text, full_date} = util.timesince(ms);
21 | if (prefix != null) text = prefix + ": " + text;
22 | let st = {};
23 | if (color != null) st.color = color;
24 | return (
25 | { text }
26 | )
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/src/js/components/common/GoogleLoginCompat.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var api = require('utils/api');
3 | import {browserHistory} from 'react-router';
4 | var UserActions = require('actions/UserActions');
5 |
6 | const GSI_CLIENT_SRC = 'https://accounts.google.com/gsi/client';
7 |
8 | const loadScript = (src) => {
9 | return new Promise((resolve, reject) => {
10 | if (document.querySelector(`script[src="${src}"]`)) return resolve()
11 | const script = document.createElement('script')
12 | script.src = src
13 | script.onload = () => resolve()
14 | script.onerror = (err) => reject(err)
15 | document.body.appendChild(script)
16 | })
17 | }
18 |
19 | export default class GoogleLoginCompat extends React.Component {
20 | constructor(props) {
21 | super(props);
22 | this.state = {
23 | signing_in: false
24 | }
25 | }
26 |
27 | componentDidMount() {
28 |
29 | const id = this.props.clientId;
30 |
31 | loadScript(GSI_CLIENT_SRC)
32 | .then(() => {
33 | /*global google*/
34 | console.log(google)
35 | google.accounts.id.initialize({
36 | client_id: id,
37 | callback: this.handleCredentialResponse.bind(this),
38 | })
39 | google.accounts.id.renderButton(
40 | this.refs.googleButton,
41 | { theme: 'outline', size: 'large' }
42 | )
43 | })
44 | .catch(console.error)
45 | }
46 |
47 | componentWillUnmount() {
48 | const scriptTag = document.querySelector(`script[src="${GSI_CLIENT_SRC}"]`);
49 | if (scriptTag) document.body.removeChild(scriptTag);
50 | }
51 |
52 | handleCredentialResponse(response) {
53 | this.verifyUser(response.credential);
54 | }
55 |
56 | verifyUser(credential) {
57 | const id_token = credential;
58 | let data = {token: id_token};
59 | this.setState({signing_in: true}, () => {
60 | api.post('/api/auth/google_login', data, (res) => {
61 | UserActions.storeUser(res.user);
62 | browserHistory.push('/app/dashboard');
63 | this.setState({signing_in: false});
64 | }, (res_fail) => {
65 | this.setState({signing_in: false});
66 | })
67 | });
68 | }
69 |
70 | render() {
71 | return (
72 |
73 | { this.state.signing_in ?
Signing in... : null }
74 |
75 |
76 | )
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/js/components/common/LoadStatus.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | import { FontIcon } from 'material-ui';
3 |
4 | export default class LoadStatus extends React.Component {
5 | getMessage() {
6 | if (this.props.loading) return this.props.loadingMessage;
7 | else if (this.props.empty) return this.props.emptyMessage;
8 | else return "--";
9 | }
10 |
11 | render() {
12 | var message = this.getMessage();
13 | var showNil = !this.props.loading && this.props.empty;
14 | var showLoadStatus = (this.props.loading || this.props.empty) && !this.props.hidden;
15 | var showLoader = this.props.loading;
16 | var loader = (
17 |
18 |
19 |
20 | );
21 | var nil = (
22 | highlight_off
23 | );
24 | return showLoadStatus ? (
25 |
26 | { showLoader ? loader : "" }
27 | { showNil ? nil : "" }
28 | { message }
29 |
30 | ) :
;
31 | }
32 | }
33 |
34 | LoadStatus.defaultProps = {
35 | loading: false,
36 | empty: true,
37 | emptyMessage: "Nothing to show",
38 | loadingMessage: "Please wait...",
39 | hidden: false
40 | };
41 |
--------------------------------------------------------------------------------
/src/js/components/common/MobileDialog.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import {Dialog} from 'material-ui';
4 |
5 | const MobileDialog = props => (
6 |
16 |
17 | {props.children}
18 |
19 |
20 | );
21 |
22 | MobileDialog.propTypes = {
23 | children: PropTypes.node,
24 | };
25 |
26 | export default MobileDialog;
--------------------------------------------------------------------------------
/src/js/components/common/ProgressLine.js:
--------------------------------------------------------------------------------
1 | var PropTypes = require('prop-types');
2 | var React = require('react');
3 | import { FontIcon } from 'material-ui';
4 | var util = require('utils/util');
5 | var ReactTooltip = require('react-tooltip');
6 |
7 | export default class ProgressLine extends React.Component {
8 | static propTypes = {
9 | value: PropTypes.number,
10 | total: PropTypes.number,
11 | color: PropTypes.string,
12 | min_color: PropTypes.string,
13 | tooltip: PropTypes.string,
14 | style: PropTypes.object,
15 | }
16 |
17 | static defaultProps = {
18 | value: 0,
19 | total: 100,
20 | color: "#4FECF9",
21 | min_color: null, // If defined, render color as per progress between min_color - color
22 | style: {},
23 | tooltip: null
24 | }
25 |
26 | componentDidMount() {
27 | if (this.props.tooltip) ReactTooltip.rebuild();
28 | }
29 |
30 | render() {
31 | let {value, total, style, tooltip, color, min_color} = this.props;
32 | let percent = total > 0 ? 100.0 * value / total : 0;
33 | if (percent > 100.0) percent = 100.0;
34 | let progress_gradient = min_color != null;
35 | let bgColor;
36 | if (progress_gradient) {
37 | bgColor = '#' + util.colorInterpolate({
38 | color1: min_color.slice(1),
39 | color2: color.slice(1),
40 | ratio: percent / 100.0
41 | })
42 | } else bgColor = color;
43 | let st = {
44 | width: percent.toFixed(1) + "%",
45 | backgroundColor: bgColor
46 | }
47 | return (
48 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/js/components/common/YearSelector.js:
--------------------------------------------------------------------------------
1 | var PropTypes = require('prop-types');
2 | var React = require('react');
3 | import Select from 'react-select'
4 |
5 | export default class YearSelector extends React.Component {
6 | static propTypes = {
7 | year: PropTypes.number,
8 | // One of following required
9 | years_back: PropTypes.number,
10 | first_year: PropTypes.number
11 | }
12 | static defaultProps = {
13 | year: null,
14 | years_back: 3
15 | }
16 |
17 | constructor(props) {
18 | super(props);
19 | this.state = {
20 | }
21 | }
22 |
23 | handleChange(val) {
24 | this.props.onChange(val)
25 | }
26 |
27 | render() {
28 | let {year, years_back, first_year} = this.props
29 | let today = new Date()
30 | let today_year = today.getFullYear()
31 | if (year == null) year = today_year
32 | let year_cursor = first_year != null ? first_year : (today_year - years_back)
33 | let year_opts = []
34 | while (year_cursor <= today_year) {
35 | year_opts.push({value: year_cursor, label: year_cursor})
36 | year_cursor += 1
37 | }
38 | return
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 | { q.text }
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 |
--------------------------------------------------------------------------------