├── assets ├── Friday.png ├── Monday.png ├── Sunday.png ├── Tuesday.png ├── Saturday.png ├── Thursday.png ├── Wednesday.png └── notion_habit_tracker_public_page.png ├── requirements.txt ├── Pipfile ├── LICENSE ├── remove_page.py ├── .gitignore ├── Pipfile.lock ├── README.md └── main.py /assets/Friday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Friday.png -------------------------------------------------------------------------------- /assets/Monday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Monday.png -------------------------------------------------------------------------------- /assets/Sunday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Sunday.png -------------------------------------------------------------------------------- /assets/Tuesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Tuesday.png -------------------------------------------------------------------------------- /assets/Saturday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Saturday.png -------------------------------------------------------------------------------- /assets/Thursday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Thursday.png -------------------------------------------------------------------------------- /assets/Wednesday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/Wednesday.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2022.9.24 2 | charset-normalizer==2.1.1 3 | idna==3.4 4 | python-dotenv==0.21.0 5 | requests==2.28.1 6 | urllib3==1.26.13 7 | -------------------------------------------------------------------------------- /assets/notion_habit_tracker_public_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashleymavericks/notion-habit-tracker/HEAD/assets/notion_habit_tracker_public_page.png -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | python-dotenv = "*" 9 | 10 | [dev-packages] 11 | 12 | [requires] 13 | python_version = "3.10" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anurag Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /remove_page.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import datetime 4 | from datetime import date, timedelta 5 | import requests 6 | import json 7 | from json import JSONEncoder 8 | 9 | # load environment variables from .env 10 | load_dotenv() 11 | 12 | base_db_url = "https://api.notion.com/v1/databases/" 13 | base_pg_url = "https://api.notion.com/v1/pages/" 14 | 15 | header = {"Authorization": os.getenv('NOTION_SECRET_KEY'), 16 | "Notion-Version": "2021-05-13", "Content-Type": "application/json"} 17 | 18 | response_habits_db = requests.post( 19 | base_db_url + os.getenv('NOTION_HABIT_DB') + "/query", headers=header) 20 | 21 | 22 | # subclass JSONEncoder 23 | class DateTimeEncoder(JSONEncoder): 24 | # Override the default method 25 | def default(self, obj): 26 | if isinstance(obj, (datetime.date, datetime.datetime)): 27 | return obj.isoformat() 28 | 29 | 30 | # Deleting previous year data, else it will mess up with monthly analytics 31 | for page in response_habits_db.json()['results']: 32 | page_id = page['id'] 33 | props = page['properties'] 34 | current_month = props['Date']['date']['start'] 35 | date_object = date.fromisoformat(current_month) 36 | 37 | if date_object.year < date.today().year: 38 | 39 | payload = { 40 | "archived": True 41 | } 42 | 43 | remove_page = requests.patch( 44 | base_pg_url + page_id, headers=header, json=payload) 45 | 46 | if remove_page.status_code == 200: 47 | print( 48 | f"Page removed, Status code: {remove_page.status_code}, Reason: {remove_page.reason}") 49 | else: 50 | print( 51 | f"Something went wrong, Status code: {remove_page.status_code}, Reason: {remove_page.reason}") 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS / VScode 2 | .vscode 3 | .DS_Store 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | test/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | env_vars.py 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "163fc9c6876d17e5df8202b86c3119d7c9709505f8c51552563e9ce41d3410dd" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", 22 | "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" 23 | ], 24 | "markers": "python_version >= '3.6'", 25 | "version": "==2022.9.24" 26 | }, 27 | "charset-normalizer": { 28 | "hashes": [ 29 | "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", 30 | "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" 31 | ], 32 | "markers": "python_full_version >= '3.6.0'", 33 | "version": "==2.1.1" 34 | }, 35 | "idna": { 36 | "hashes": [ 37 | "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", 38 | "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" 39 | ], 40 | "markers": "python_version >= '3.5'", 41 | "version": "==3.4" 42 | }, 43 | "python-dotenv": { 44 | "hashes": [ 45 | "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5", 46 | "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045" 47 | ], 48 | "index": "pypi", 49 | "version": "==0.21.0" 50 | }, 51 | "requests": { 52 | "hashes": [ 53 | "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", 54 | "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" 55 | ], 56 | "index": "pypi", 57 | "version": "==2.28.1" 58 | }, 59 | "urllib3": { 60 | "hashes": [ 61 | "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", 62 | "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" 63 | ], 64 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 65 | "version": "==1.26.13" 66 | } 67 | }, 68 | "develop": {} 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Notion Habit Tracker

3 | License: MIT

4 | A fully automated Notion Habit Tracker template with real-time performance graph and monthly analytics.

5 | 6 | [![notion_habit_tracket](/assets/notion_habit_tracker_public_page.png)](https://ashleymavericks.gumroad.com/l/notion-habit-tracker) 7 |
8 | 9 | *** 10 | ## Features 11 | - Automated daily page addition, either via Notion template repeat feature or by running the Python script once a year 12 | - Script will automatically delete any previous year pages, so they won't mess up with monthly analytics 13 | - Monthly analytics provide Habits completion % insights via star ratings ⭐️ 14 | - Real-time daily performance charts 🚀 15 | - Habit completion visual cues using progress bar and custom star ratings 16 | - Notion formulae are used in a way, that user will only see current week and current monthly graph performance 17 | 18 | ## Demo Video 19 | [](https://youtu.be/tDbUbqv4S0c) 20 | 21 | ## Setup Pre-requisites 22 | - Duplicate this [Notion Template](https://ashleymavericks.gumroad.com/l/notion-habit-tracker) to get started. 23 | - Use services like [ChartBase](https://www.chartbase.so/) and [Data Jumbo](https://www.datajumbo.co/) to have the monthly analytics chart for your habits database. 24 | 25 | ## No Code Approach 26 | - With the new notion recurring template update, you can simply set an individual weekday template for a weekly repeat and it will automatically add a new page to the database. 27 | 28 | ## Find Database ID 29 | Login Notion in a browser and viewing the database as a full page, the database ID is the part of the URL after your workspace name and the slash (acme/) and before the question mark (?). The ID is 32 characters long, containing numbers and letters. Copy the ID and paste it somewhere you can easily find later. 30 | 31 | Repeat this process for both Habits DB and Analytics DB, and take a note of these Database IDs 32 | 33 | ``` 34 | https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=... 35 | |--------- Database ID --------| 36 | ``` 37 | 38 | ## Script Usage 39 | 1. Clone the repo 40 | ```bash 41 | git clone https://github.com/ashleymavericks/notion-habit-tracker.git 42 | ``` 43 | 2. Create a .env file in the project folder 44 | 3. Add NOTION_HABIT_DB, NOTION_ANALYTICS_DB and [NOTION_SECRET_KEY](https://syncwith.com/p/notion-api-key-qrsJHMnH5LuHUjDqvZnmWC) in .env file 45 | ```bash 46 | NOTION_SECRET_KEY=PASTE_KEY_HERE 47 | NOTION_HABIT_DB=PASTE_KEY_HERE 48 | NOTION_ANALYTICS_DB=PASTE_KEY_HERE 49 | ``` 50 | 4. Install project dependencies 51 | ```bash 52 | pipenv install 53 | ``` 54 | 5. Run the Script called `main.py`, it may take some time to complete 55 | ```bash 56 | python3 main.py 57 | ``` 58 | 6. Due to Notion API rate throttling, the script might not able to remove a huge number of previous year records at once, in that scenario kindly run `remove_page.py` until all records get purged 59 | ```bash 60 | python3 remove_page.py 61 | ``` 62 | 63 | ## Contributing 64 | Feel free to reach out, if you want to further improve the template. 65 | 66 | ## License 67 | This project is licensed under the MIT license 68 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | import datetime 4 | from datetime import date, timedelta 5 | import requests 6 | import json 7 | from json import JSONEncoder 8 | 9 | # load environment variables from .env 10 | load_dotenv() 11 | 12 | base_db_url = "https://api.notion.com/v1/databases/" 13 | base_pg_url = "https://api.notion.com/v1/pages/" 14 | 15 | header = {"Authorization": os.getenv('NOTION_SECRET_KEY'), 16 | "Notion-Version": "2021-05-13", "Content-Type": "application/json"} 17 | 18 | notion_habit_db = os.getenv('NOTION_HABIT_DB') 19 | notion_analytics_db = os.getenv('NOTION_ANALYTICS_DB') 20 | 21 | response_habits_db = requests.post( 22 | base_db_url + notion_habit_db + "/query", headers=header) 23 | 24 | response_analytics_db = requests.post( 25 | base_db_url + notion_analytics_db + "/query", headers=header) 26 | 27 | # define no. of new pages/records to be added in Tracker 28 | days_count = 365 29 | 30 | # subclass JSONEncoder 31 | class DateTimeEncoder(JSONEncoder): 32 | # Override the default method 33 | def default(self, obj): 34 | if isinstance(obj, (datetime.date, datetime.datetime)): 35 | return obj.isoformat() 36 | 37 | date_list = list() 38 | month_dict = dict() 39 | 40 | # Deleting previous year data, else it will mess up with monthly analytics 41 | for page in response_habits_db.json()['results']: 42 | page_id = page['id'] 43 | props = page['properties'] 44 | current_month = props['Date']['date']['start'] 45 | date_object = date.fromisoformat(current_month) 46 | 47 | if date_object.year < date.today().year: 48 | 49 | payload = { 50 | "archived": True 51 | } 52 | 53 | remove_page = requests.patch( 54 | base_pg_url + page_id, headers=header, json=payload) 55 | 56 | if remove_page.status_code == 200: 57 | print( 58 | f"Page removed, Status code: {remove_page.status_code}, Reason: {remove_page.reason}") 59 | else: 60 | print( 61 | f"Something went wrong, Status code: {remove_page.status_code}, Reason: {remove_page.reason}") 62 | 63 | 64 | # Creating list of datetime.date objects for current month pages/records 65 | for page in response_habits_db.json()['results']: 66 | page_id = page['id'] 67 | props = page['properties'] 68 | current_month = props['Date']['date']['start'] 69 | date_object = date.fromisoformat(current_month) 70 | date_list.append(date_object) 71 | 72 | if not date_list: 73 | start_date = date.today() 74 | else: 75 | start_date = max(date_list) + timedelta(days=1) 76 | 77 | 78 | # Creating dict of page name and id for monthly analytics db 79 | for page in response_analytics_db.json()['results']: 80 | page_id = page['id'] 81 | props = page['properties'] 82 | page_name = props['Name']['title'][0]['text']['content'] 83 | month_dict[page_name] = page_id 84 | 85 | for date in (start_date + timedelta(n) for n in range(days_count)): 86 | day = date.strftime('%A') 87 | month = date.strftime('%B') 88 | relation_id = month_dict[month] 89 | date_string = date.strftime('%B %d, %Y').replace(' 0', ' ') 90 | 91 | # To make datetime.date object JSON serializable 92 | date_json = json.dumps(date, indent=4, cls=DateTimeEncoder) 93 | date_modified = date_json[1:11] 94 | 95 | payload = { 96 | "parent": { 97 | "database_id": notion_habit_db 98 | }, 99 | "cover": { 100 | "type": "external", 101 | "external": { 102 | "url": "https://github.com/ashleymavericks/notion-habit-tracker/blob/main/assets/" + day + ".png?raw=true" 103 | } 104 | }, 105 | "properties": { 106 | "Date": { 107 | "date": {"start": date_modified} 108 | }, 109 | "Name": { 110 | "title": [ 111 | { 112 | "text": { 113 | "content": date_string 114 | } 115 | } 116 | ] 117 | }, 118 | "Relation": { 119 | "relation": [ 120 | { 121 | "id": relation_id 122 | } 123 | ] 124 | } 125 | } 126 | } 127 | 128 | add_page = requests.post( 129 | base_pg_url, headers=header, json=payload) 130 | 131 | if add_page.status_code == 200: 132 | print( 133 | f"Page added, Status code: {add_page.status_code}, Reason: {add_page.reason}") 134 | else: 135 | print( 136 | f"Something went wrong, Status code: {add_page.status_code}, Reason: {add_page.reason}") 137 | --------------------------------------------------------------------------------