diff --git a/.gitignore b/.gitignore index a14f5e8..79c08f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ docs/_build/ .coverage htmlcov/ *~ +.idea + +django_fitapp \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3d6c0c7..f28e06a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,9 @@ +0.4.0 (2018-01-10) +------------------ + +- Adds Django 2.0.1 support +- Updates dependencies + 0.3.0 (2017-01-25) ------------------ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..872dd5a --- /dev/null +++ b/Pipfile @@ -0,0 +1,28 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +fitbit = ">=0.3.0" +celery = "*" +simplejson = "<4,>=3.13" +mock = "<3,>=2" +freezegun = "<0.4,>=0.3.9" +requests-mock = "<1.5,>=1.4.0" +Django = "<2.1,>=2.0" + +[dev-packages] +six = "*" +coverage = "*" +django-debug-toolbar = "*" +tox = "*" +Sphinx = "*" +pip-review = "*" +pipdeptree = "*" + +[requires] +python_version = "3.6" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..649e576 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,444 @@ +{ + "_meta": { + "hash": { + "sha256": "da6426b821f8b973356d16c93ef5ab1f1f38fac949c20bc50c4a6c3dd9ef554b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "amqp": { + "hashes": [ + "sha256:073dd02fdd73041bffc913b767866015147b61f2a9bc104daef172fc1a0066eb", + "sha256:eed41946890cd43e8dee44a316b85cf6fee5a1a34bb4a562b660a358eb529e1b" + ], + "version": "==2.3.2" + }, + "billiard": { + "hashes": [ + "sha256:ed65448da5877b5558f19d2f7f11f8355ea76b3e63e1c0a6059f47cfae5f1c84" + ], + "version": "==3.5.0.4" + }, + "celery": { + "hashes": [ + "sha256:77dab4677e24dc654d42dfbdfed65fa760455b6bb563a0877ecc35f4cfcfc678", + "sha256:ad7a7411772b80a4d6c64f2f7f723200e39fb66cf614a7fdfab76d345acc7b13" + ], + "index": "pypi", + "version": "==4.2.1" + }, + "certifi": { + "hashes": [ + "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", + "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + ], + "version": "==2018.4.16" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "django": { + "hashes": [ + "sha256:97886b8a13bbc33bfeba2ff133035d3eca014e2309dff2b6da0bdfc0b8656613", + "sha256:e900b73beee8977c7b887d90c6c57d68af10066b9dac898e1eaf0f82313de334" + ], + "index": "pypi", + "version": "==2.0.7" + }, + "fitbit": { + "hashes": [ + "sha256:9a88a790659c318762d1a30ad3d831393044f3040ca5d24959ca14eb3c1e256a" + ], + "index": "pypi", + "version": "==0.3.0" + }, + "freezegun": { + "hashes": [ + "sha256:703caac155dcaad61f78de4cb0666dca778d854dfb90b3699930adee0559a622", + "sha256:94c59d69bb99c9ec3ca5a3adb41930d3ea09d2a9756c23a02d89fa75646e78dd" + ], + "index": "pypi", + "version": "==0.3.10" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "kombu": { + "hashes": [ + "sha256:86adec6c60f63124e2082ea8481bbe4ebe04fde8ebed32c177c7f0cd2c1c9082", + "sha256:b274db3a4eacc4789aeb24e1de3e460586db7c4fc8610f7adcc7a3a1709a60af" + ], + "version": "==4.2.1" + }, + "mock": { + "hashes": [ + "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", + "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "oauthlib": { + "hashes": [ + "sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162", + "sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b" + ], + "version": "==2.1.0" + }, + "pbr": { + "hashes": [ + "sha256:754e766b4f4bad3aa68cfd532456298da1aa39375da8748392dbae90860d5f18", + "sha256:c6bddbad814f23c7faaf88d8a186e9965243cc6206a23361b73023648e645794" + ], + "version": "==4.1.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", + "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + ], + "version": "==2.7.3" + }, + "pytz": { + "hashes": [ + "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", + "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + ], + "version": "==2018.5" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "version": "==2.19.1" + }, + "requests-mock": { + "hashes": [ + "sha256:2931887853c42e1d73879983d5bf03041109472991c5b4b8dba5d11ed23b9d0b", + "sha256:96a1e45b1c0bd18d14fcb2d55b3b09d6d46237e37bcae3155df4cb75bc42619e" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:8886bfec5ad7afb391ed5443b1f697c6f4ae98d0e5620839d8b4499c032ada3f", + "sha256:e21232e2465808c0e892e0e4dbb8c2faafec16ac6dc067dd546e9b466f3deac8", + "sha256:fe3282f48fb134ee0035712159f5429215459407f6d5484013343031ff1a400d" + ], + "version": "==1.0.0" + }, + "simplejson": { + "hashes": [ + "sha256:067a7177ddfa32e1483ba5169ebea1bc2ea27f224853211ca669325648ca5642", + "sha256:2b8cb601d9ba0381499db719ccc9dfbb2fbd16013f5ff096b1a68a4775576a04", + "sha256:2c139daf167b96f21542248f8e0a06596c9b9a7a41c162cc5c9ee9f3833c93cd", + "sha256:2fc546e6af49fb45b93bbe878dea4c48edc34083729c0abd09981fe55bdf7f91", + "sha256:354fa32b02885e6dae925f1b5bbf842c333c1e11ea5453ddd67309dc31fdb40a", + "sha256:37e685986cf6f8144607f90340cff72d36acf654f3653a6c47b84c5c38d00df7", + "sha256:3af610ee72efbe644e19d5eaad575c73fb83026192114e5f6719f4901097fce2", + "sha256:3b919fc9cf508f13b929a9b274c40786036b31ad28657819b3b9ba44ba651f50", + "sha256:3dd289368bbd064974d9a5961101f080e939cbe051e6689a193c99fb6e9ac89b", + "sha256:491de7acc423e871a814500eb2dcea8aa66c4a4b1b4825d18f756cdf58e370cb", + "sha256:495511fe5f10ccf4e3ed4fc0c48318f533654db6c47ecbc970b4ed215c791968", + "sha256:65b41a5cda006cfa7c66eabbcf96aa704a6be2a5856095b9e2fd8c293bad2b46", + "sha256:6c3258ffff58712818a233b9737fe4be943d306c40cf63d14ddc82ba563f483a", + "sha256:75e3f0b12c28945c08f54350d91e624f8dd580ab74fd4f1bbea54bc6b0165610", + "sha256:79b129fe65fdf3765440f7a73edaffc89ae9e7885d4e2adafe6aa37913a00fbb", + "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5", + "sha256:c206f47cbf9f32b573c9885f0ec813d2622976cf5effcf7e472344bc2e020ac1", + "sha256:d8e238f20bcf70063ee8691d4a72162bcec1f4c38f83c93e6851e72ad545dabb", + "sha256:ee9625fc8ee164902dfbb0ff932b26df112da9f871c32f0f9c1bcf20c350fe2a", + "sha256:fb2530b53c28f0d4d84990e945c2ebb470edb469d63e389bf02ff409012fe7c5", + "sha256:feadb95170e45f439455354904768608e356c5b174ca30b3d11b0e3f24b5c0df" + ], + "index": "pypi", + "version": "==3.16.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "version": "==1.23" + }, + "vine": { + "hashes": [ + "sha256:52116d59bc45392af9fdd3b75ed98ae48a93e822cee21e5fda249105c59a7a72", + "sha256:6849544be74ec3638e84d90bc1cf2e1e9224cc10d96cd4383ec3f69e9bce077b" + ], + "version": "==1.1.4" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456", + "sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7" + ], + "version": "==0.7.11" + }, + "babel": { + "hashes": [ + "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", + "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + ], + "version": "==2.6.0" + }, + "certifi": { + "hashes": [ + "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", + "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + ], + "version": "==2018.4.16" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:10cfac276cf3dd0acefc49444fc4e1a0a4c23c855d9fcbd555681c3a47a328e6", + "sha256:18797137634b64fe488b239d3709e5f8fdea80aea09f86ec819c633a2c84f79c", + "sha256:1a54b37e265dd81922f32eff50559630905770cdf8a8e560aa5a4f3297e5d5bf", + "sha256:245709d580be9c7a5f8e2aeebab889f571ac323ff34bdde497072e82c0295546", + "sha256:316881a28d2a1a5853495092267fcacf245805b4139f0fc996f8a6c4be6fb499", + "sha256:3368098e2c633ec6b2af4f91abde94b5c3b8fa66857452137485f40be77aeda6", + "sha256:33e0aa553d256b0daf43e0026db3bd415eb4b94c8dc7984afb84c10efa51a83b", + "sha256:35fe7a6c06851c4c6a4c171eb796d27e023f5a1ce1e25837ea720f5b8cb76fce", + "sha256:3a1c8ed67a64627ef317de64356731f8f173b76457672e933db896c080e1cc2b", + "sha256:3e79318f0ddb197e775a742cc44807b1e9f3b8a57325f422fe547d3e0ca01b86", + "sha256:59fa7e9857205b8d6f6fce0eaea07409bcdffd68eaec3db7e0b1ac720d4fe0f3", + "sha256:6b2e2ef7572b399b0cc2f6d05c06ada40329166d6fc58beef8081fb94a41201f", + "sha256:712599fc602c302c540fe7e83b6d82aaf381ec5bfb4a51dc5c30f57d214d649f", + "sha256:773c0e658503538554516f5f901e775cda760648d8d2b988e16f187812c0c089", + "sha256:7c8dbbc9e5480856125511f11a5c735cff3200e367adc3ba342dad506a25407d", + "sha256:7fc25906ecb0a6af0c434370da6cfbcf8badb257c5cf9a6464f5e37fe4ebc949", + "sha256:88d81556e00ac7e1cc9e70a2376859f41e46d187b6dd5883422aa537505f8a98", + "sha256:91a915f5fc88db7adace367e8ef65d1a418d29f7ade62514d604eed87c861355", + "sha256:9f696b90ff4886ba5a277995397a13b0600bfd97c70d8ae4241c2aecea11ee61", + "sha256:a863f4540446d7eeaf6bf716aee277eaf38842718e86bdb80cdca78cdf1fed0d", + "sha256:ab3981817dcec2dd9ea552e46538ee2e34480ec623fc365019ddae82bc9be143", + "sha256:b3b6d8d8194e7e1300240402dfd9c54840d03621e69da821d8ffc8bbebe00137", + "sha256:c296ac03ba12e184bef03387d89c4a0be79daff214294917ce77df32240bf4d8", + "sha256:c75b3de73cc7ba2e911a907322c65dd10da216f37e7477f22dbd0098775f6345", + "sha256:c87c9ee13ce431305734b8e3f0bf00468a1d4f4ee60b6ef63c69282776ab94d6", + "sha256:c89c895ff5cfda45a5f681514b647986f76a4f984df125d210c154e5a1a2472b", + "sha256:c9fa8fbda281b1ddf25b8fa7ccf0564198a86c9da8a413111fcadd510a98a232", + "sha256:ccdf1bd8fd848690fb3d5153d0c54c41169e59804acb9652664f5f669fe25c11" + ], + "index": "pypi", + "version": "==5.0a1" + }, + "django": { + "hashes": [ + "sha256:97886b8a13bbc33bfeba2ff133035d3eca014e2309dff2b6da0bdfc0b8656613", + "sha256:e900b73beee8977c7b887d90c6c57d68af10066b9dac898e1eaf0f82313de334" + ], + "index": "pypi", + "version": "==2.0.7" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:4af2a4e1e932dadbda197b18585962d4fc20172b4e5a479490bc659fe998864d", + "sha256:d9ea75659f76d8f1e3eb8f390b47fc5bad0908d949c34a8a3c4c87978eb40a0f" + ], + "index": "pypi", + "version": "==1.9.1" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "imagesize": { + "hashes": [ + "sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18", + "sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315" + ], + "version": "==1.0.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "packaging": { + "hashes": [ + "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", + "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" + ], + "version": "==17.1" + }, + "pip-review": { + "hashes": [ + "sha256:aa368c457e4b097d0cfbf9024eb574c57883177bc38d89c75eafc4e58174a82e", + "sha256:e4b27cdf0d74b519abc792eb4ab0d92471f1a378efa0f5e382ff9c7ad69916ab" + ], + "index": "pypi", + "version": "==1.0" + }, + "pipdeptree": { + "hashes": [ + "sha256:013d343fb0305e95f33a81329a30277fcaac45f78ccea90bcfcdb7dbb9d13da2", + "sha256:2cdd29356c9e3a0cab60d1b20571de713abca031a87f4685c31fc0cab3295d19", + "sha256:a2774940d77fa11c1fb275c350080e75c592d1db5ff5679e0be5e566239de83a" + ], + "index": "pypi", + "version": "==0.13.0" + }, + "pluggy": { + "hashes": [ + "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", + "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", + "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", + "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" + ], + "version": "==1.5.4" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "pyparsing": { + "hashes": [ + "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", + "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", + "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", + "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", + "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", + "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58", + "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" + ], + "version": "==2.2.0" + }, + "pytz": { + "hashes": [ + "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", + "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + ], + "version": "==2018.5" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "version": "==2.19.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + ], + "version": "==1.2.1" + }, + "sphinx": { + "hashes": [ + "sha256:217ad9ece2156ed9f8af12b5d2c82a499ddf2c70a33c5f81864a08d8c67b9efc", + "sha256:a765c6db1e5b62aae857697cd4402a5c1a315a7b0854bbcd0fc8cdc524da5896" + ], + "index": "pypi", + "version": "==1.7.6" + }, + "sphinxcontrib-websupport": { + "hashes": [ + "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", + "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" + ], + "version": "==1.1.0" + }, + "sqlparse": { + "hashes": [ + "sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec", + "sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4" + ], + "version": "==0.2.4" + }, + "tox": { + "hashes": [ + "sha256:4df108a1fcc93a7ee4ac97e1a3a1fc3d41ddd22445d518976604e2ef05025280", + "sha256:9f0cbcc36e08c2c4ae90d02d3d1f9a62231f974bcbc1df85e8045946d8261059" + ], + "index": "pypi", + "version": "==3.1.2" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "version": "==1.23" + }, + "virtualenv": { + "hashes": [ + "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", + "sha256:ca07b4c0b54e14a91af9f34d0919790b016923d157afda5efdde55c96718f752" + ], + "version": "==16.0.0" + } + } +} diff --git a/fitapp/decorators.py b/fitapp/decorators.py index 0aa981c..23e93fb 100644 --- a/fitapp/decorators.py +++ b/fitapp/decorators.py @@ -42,6 +42,7 @@ def my_view(request): """ if not msg: msg = utils.get_setting('FITAPP_DECORATOR_MESSAGE') + def inner_decorator(view_func): def wrapped(request, *args, **kwargs): user = request.user @@ -49,5 +50,7 @@ def wrapped(request, *args, **kwargs): text = msg(request) if callable(msg) else msg messages.error(request, text) return view_func(request, *args, **kwargs) + return wraps(view_func)(wrapped) + return inner_decorator diff --git a/fitapp/defaults.py b/fitapp/defaults.py index 66d8f7f..aca1514 100644 --- a/fitapp/defaults.py +++ b/fitapp/defaults.py @@ -41,3 +41,8 @@ # called with the request as the only parameter to get the final value for the # message. FITAPP_DECORATOR_MESSAGE = 'This page requires Fitbit integration.' + +# By default, don't try to get intraday time series data. See +# https://dev.fitbit.com/docs/activity/#get-activity-intraday-time-series for +# more info. +FITAPP_GET_INTRADAY = False diff --git a/fitapp/fixtures/initial_data.json b/fitapp/fixtures/initial_data.json index 49f53c0..ef9381b 100644 --- a/fitapp/fixtures/initial_data.json +++ b/fitapp/fixtures/initial_data.json @@ -4,7 +4,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "calories" + "resource": "calories", + "intraday_support": "False" } }, { @@ -12,7 +13,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "caloriesBMR" + "resource": "caloriesBMR", + "intraday_support": "False" } }, { @@ -20,7 +22,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "steps" + "resource": "steps", + "intraday_support": "False" } }, { @@ -28,7 +31,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "distance" + "resource": "distance", + "intraday_support": "False" } }, { @@ -36,7 +40,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "floors" + "resource": "floors", + "intraday_support": "False" } }, { @@ -44,7 +49,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "elevation" + "resource": "elevation", + "intraday_support": "False" } }, { @@ -52,7 +58,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "minutesSedentary" + "resource": "minutesSedentary", + "intraday_support": "False" } }, { @@ -60,7 +67,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "minutesLightlyActive" + "resource": "minutesLightlyActive", + "intraday_support": "False" } }, { @@ -68,7 +76,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "minutesFairlyActive" + "resource": "minutesFairlyActive", + "intraday_support": "False" } }, { @@ -76,7 +85,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "minutesVeryActive" + "resource": "minutesVeryActive", + "intraday_support": "False" } }, { @@ -84,7 +94,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "activeScore" + "resource": "activeScore", + "intraday_support": "False" } }, { @@ -92,7 +103,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "activityCalories" + "resource": "activityCalories", + "intraday_support": "False" } }, { @@ -100,7 +112,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/minutesSedentary" + "resource": "tracker/minutesSedentary", + "intraday_support": "False" } }, { @@ -108,7 +121,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/minutesLightlyActive" + "resource": "tracker/minutesLightlyActive", + "intraday_support": "False" } }, { @@ -116,7 +130,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/minutesFairlyActive" + "resource": "tracker/minutesFairlyActive", + "intraday_support": "False" } }, { @@ -124,7 +139,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/minutesVeryActive" + "resource": "tracker/minutesVeryActive", + "intraday_support": "False" } }, { @@ -132,7 +148,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/activeScore" + "resource": "tracker/activeScore", + "intraday_support": "False" } }, { @@ -140,7 +157,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/activityCalories" + "resource": "tracker/activityCalories", + "intraday_support": "False" } }, { @@ -148,7 +166,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/calories" + "resource": "tracker/calories", + "intraday_support": "False" } }, { @@ -156,7 +175,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/steps" + "resource": "tracker/steps", + "intraday_support": "False" } }, { @@ -164,7 +184,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/distance" + "resource": "tracker/distance", + "intraday_support": "False" } }, { @@ -172,7 +193,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/floors" + "resource": "tracker/floors", + "intraday_support": "False" } }, { @@ -180,7 +202,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 1, - "resource": "tracker/elevation" + "resource": "tracker/elevation", + "intraday_support": "False" } }, { @@ -188,7 +211,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 3, - "resource": "weight" + "resource": "weight", + "intraday_support": "False" } }, { @@ -196,7 +220,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 3, - "resource": "bmi" + "resource": "bmi", + "intraday_support": "False" } }, { @@ -204,7 +229,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 3, - "resource": "fat" + "resource": "fat", + "intraday_support": "False" } }, { @@ -212,7 +238,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "startTime" + "resource": "startTime", + "intraday_support": "False" } }, { @@ -220,7 +247,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "timeInBed" + "resource": "timeInBed", + "intraday_support": "False" } }, { @@ -228,7 +256,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "minutesAsleep" + "resource": "minutesAsleep", + "intraday_support": "False" } }, { @@ -236,7 +265,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "awakeningsCount" + "resource": "awakeningsCount", + "intraday_support": "False" } }, { @@ -244,7 +274,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "minutesAwake" + "resource": "minutesAwake", + "intraday_support": "False" } }, { @@ -252,7 +283,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "minutesToFallAsleep" + "resource": "minutesToFallAsleep", + "intraday_support": "False" } }, { @@ -260,7 +292,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "minutesAfterWakeup" + "resource": "minutesAfterWakeup", + "intraday_support": "False" } }, { @@ -268,7 +301,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 2, - "resource": "efficiency" + "resource": "efficiency", + "intraday_support": "False" } }, { @@ -276,7 +310,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 0, - "resource": "log/caloriesIn" + "resource": "log/caloriesIn", + "intraday_support": "False" } }, { @@ -284,7 +319,8 @@ "model": "fitapp.timeseriesdatatype", "fields": { "category": 0, - "resource": "log/water" + "resource": "log/water", + "intraday_support": "False" } } ] diff --git a/fitapp/forms.py b/fitapp/forms.py index 04e36ba..19fb752 100644 --- a/fitapp/forms.py +++ b/fitapp/forms.py @@ -2,7 +2,6 @@ from . import utils - INPUT_FORMATS = ['%Y-%m-%d'] diff --git a/fitapp/management/commands/refresh_tokens.py b/fitapp/management/commands/refresh_tokens.py index ef72bba..178ccd9 100644 --- a/fitapp/management/commands/refresh_tokens.py +++ b/fitapp/management/commands/refresh_tokens.py @@ -13,7 +13,7 @@ import time -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from oauthlib.oauth2.rfc6749.errors import InvalidGrantError from fitapp.models import UserFitbit diff --git a/fitapp/migrations/0001_initial.py b/fitapp/migrations/0001_initial.py index 926b26d..d8bdfaf 100644 --- a/fitapp/migrations/0001_initial.py +++ b/fitapp/migrations/0001_initial.py @@ -1,21 +1,25 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import models, migrations from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): + UserModel = getattr(settings, 'FITAPP_USER_MODEL', 'auth.User') + dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + migrations.swappable_dependency(UserModel), ] operations = [ migrations.CreateModel( name='TimeSeriesData', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), ('date', models.DateField()), ('value', models.CharField(default=None, max_length=32, null=True)), ], @@ -26,9 +30,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='TimeSeriesDataType', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('category', models.IntegerField(choices=[(0, b'foods'), (1, b'activities'), (2, b'sleep'), (3, b'body')])), + ('id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), + ('category', models.IntegerField( + choices=[(0, b'foods'), (1, b'activities'), (2, b'sleep'), + (3, b'body')])), ('resource', models.CharField(max_length=128)), + ('intraday_support', models.BooleanField(default=False)), ], options={ 'ordering': ['category', 'resource'], @@ -38,11 +47,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserFitbit', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('id', + models.AutoField(verbose_name='ID', serialize=False, auto_created=True, + primary_key=True)), ('fitbit_user', models.CharField(unique=True, max_length=32)), ('auth_token', models.TextField()), ('auth_secret', models.TextField()), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField(to=UserModel, + on_delete=models.CASCADE)), ], options={ }, @@ -55,13 +67,15 @@ class Migration(migrations.Migration): migrations.AddField( model_name='timeseriesdata', name='resource_type', - field=models.ForeignKey(to='fitapp.TimeSeriesDataType'), + field=models.ForeignKey(to='fitapp.TimeSeriesDataType', + on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='timeseriesdata', name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=UserModel, + on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( diff --git a/fitapp/migrations/0002_initial_data.py b/fitapp/migrations/0002_initial_data.py index 8d3b913..d6fcb6b 100644 --- a/fitapp/migrations/0002_initial_data.py +++ b/fitapp/migrations/0002_initial_data.py @@ -2,13 +2,14 @@ from __future__ import unicode_literals import os -from sys import path + from django.core import serializers -from django.db import models, migrations +from django.db import migrations fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures')) fixture_filename = 'initial_data.json' + def load_fixture(apps, schema_editor): fixture_file = os.path.join(fixture_dir, fixture_filename) @@ -18,6 +19,7 @@ def load_fixture(apps, schema_editor): obj.save() fixture.close() + def unload_fixture(apps, schema_editor): "Brutally deleting all entries for this model..." @@ -26,7 +28,6 @@ def unload_fixture(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('fitapp', '0001_initial'), ] diff --git a/fitapp/migrations/0003_add_refresh_token_field.py b/fitapp/migrations/0003_add_refresh_token_field.py index 6419fff..e5c5174 100644 --- a/fitapp/migrations/0003_add_refresh_token_field.py +++ b/fitapp/migrations/0003_add_refresh_token_field.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ('fitapp', '0002_initial_data'), ] diff --git a/fitapp/migrations/0004_rename_auth_token_to_access_token.py b/fitapp/migrations/0004_rename_auth_token_to_access_token.py index 4561cc4..05633b0 100644 --- a/fitapp/migrations/0004_rename_auth_token_to_access_token.py +++ b/fitapp/migrations/0004_rename_auth_token_to_access_token.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('fitapp', '0003_add_refresh_token_field'), ] diff --git a/fitapp/migrations/0005_upgrade_oauth1_tokens_to_oauth2.py b/fitapp/migrations/0005_upgrade_oauth1_tokens_to_oauth2.py index f12ef78..7168e11 100644 --- a/fitapp/migrations/0005_upgrade_oauth1_tokens_to_oauth2.py +++ b/fitapp/migrations/0005_upgrade_oauth1_tokens_to_oauth2.py @@ -1,9 +1,9 @@ -from django.db import migrations, models - +from django.db import migrations from fitbit.api import FitbitOauth2Client -from fitapp.utils import get_setting from oauthlib.oauth2.rfc6749.errors import MissingTokenError +from fitapp.utils import get_setting + def forwards(apps, schema_editor): UserFitbit = apps.get_model('fitapp', 'UserFitbit') @@ -29,7 +29,6 @@ def backwards(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('fitapp', '0004_rename_auth_token_to_access_token'), ] diff --git a/fitapp/migrations/0006_help_text.py b/fitapp/migrations/0006_help_text.py index 630106c..f8ef04d 100644 --- a/fitapp/migrations/0006_help_text.py +++ b/fitapp/migrations/0006_help_text.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): + UserModel = getattr(settings, 'FITAPP_USER_MODEL', 'auth.User') dependencies = [ ('fitapp', '0005_upgrade_oauth1_tokens_to_oauth2'), + migrations.swappable_dependency(UserModel), ] operations = [ @@ -20,27 +22,37 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='timeseriesdata', name='resource_type', - field=models.ForeignKey(help_text=b'The type of time series data', to='fitapp.TimeSeriesDataType'), + field=models.ForeignKey(help_text=b'The type of time series data', + to='fitapp.TimeSeriesDataType', + on_delete=models.CASCADE), ), migrations.AlterField( model_name='timeseriesdata', name='user', - field=models.ForeignKey(help_text=b"The data's user", to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(help_text=b"The data's user", + to=UserModel, + on_delete=models.CASCADE + ), ), migrations.AlterField( model_name='timeseriesdata', name='value', - field=models.CharField(default=None, max_length=32, null=True, help_text=b'The value of the data. This is typically a number, though saved as a string here. The units can be inferred from the data type. For example, for step data the value might be "9783" (the units) would be "steps"'), + field=models.CharField(default=None, max_length=32, null=True, + help_text=b'The value of the data. This is typically a number, though saved as a string here. The units can be inferred from the data type. For example, for step data the value might be "9783" (the units) would be "steps"'), ), migrations.AlterField( model_name='timeseriesdatatype', name='category', - field=models.IntegerField(help_text=b'The category of the time series data, one of: 0(foods), 1(activities), 2(sleep), 3(body)', choices=[(0, b'foods'), (1, b'activities'), (2, b'sleep'), (3, b'body')]), + field=models.IntegerField( + help_text=b'The category of the time series data, one of: 0(foods), 1(activities), 2(sleep), 3(body)', + choices=[(0, b'foods'), (1, b'activities'), (2, b'sleep'), (3, b'body')]), ), migrations.AlterField( model_name='timeseriesdatatype', name='resource', - field=models.CharField(help_text=b'The specific time series resource. This is the string that will be used for the [resource-path] of the API url referred to in the Fitbit documentation', max_length=128), + field=models.CharField( + help_text=b'The specific time series resource. This is the string that will be used for the [resource-path] of the API url referred to in the Fitbit documentation', + max_length=128), ), migrations.AlterField( model_name='userfitbit', @@ -55,7 +67,8 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='userfitbit', name='fitbit_user', - field=models.CharField(help_text=b'The fitbit user ID', unique=True, max_length=32), + field=models.CharField(help_text=b'The fitbit user ID', unique=True, + max_length=32), ), migrations.AlterField( model_name='userfitbit', @@ -65,6 +78,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='userfitbit', name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL, help_text=b'The user'), + field=models.OneToOneField(to=UserModel, help_text=b'The user', + on_delete=models.CASCADE), ), ] diff --git a/fitapp/migrations/0007_userfitbit_expires_at.py b/fitapp/migrations/0007_userfitbit_expires_at.py index 5f8f5e8..a59c01c 100644 --- a/fitapp/migrations/0007_userfitbit_expires_at.py +++ b/fitapp/migrations/0007_userfitbit_expires_at.py @@ -8,7 +8,6 @@ class Migration(migrations.Migration): - dependencies = [ ('fitapp', '0006_help_text'), ] @@ -17,7 +16,8 @@ class Migration(migrations.Migration): migrations.AddField( model_name='userfitbit', name='expires_at', - field=models.FloatField(default=time.time(), help_text=b'The timestamp when the access token expires'), + field=models.FloatField(default=time.time(), + help_text=b'The timestamp when the access token expires'), preserve_default=False, ), ] diff --git a/fitapp/migrations/0008_remove_userfitbit_auth_secret.py b/fitapp/migrations/0008_remove_userfitbit_auth_secret.py index 7ea5bdd..bd3f9b4 100644 --- a/fitapp/migrations/0008_remove_userfitbit_auth_secret.py +++ b/fitapp/migrations/0008_remove_userfitbit_auth_secret.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('fitapp', '0007_userfitbit_expires_at'), ] diff --git a/fitapp/migrations/0009_auto_20180110_1605.py b/fitapp/migrations/0009_auto_20180110_1605.py new file mode 100644 index 0000000..dee1acc --- /dev/null +++ b/fitapp/migrations/0009_auto_20180110_1605.py @@ -0,0 +1,85 @@ +# Generated by Django 2.0.1 on 2018-01-10 16:05 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + UserModel = getattr(settings, 'FITAPP_USER_MODEL', 'auth.User') + dependencies = [ + ('fitapp', '0008_remove_userfitbit_auth_secret'), + migrations.swappable_dependency(UserModel), + ] + + operations = [ + migrations.AlterField( + model_name='timeseriesdata', + name='date', + field=models.DateField(help_text='The date the data was recorded'), + ), + migrations.AlterField( + model_name='timeseriesdata', + name='resource_type', + field=models.ForeignKey(help_text='The type of time series data', + on_delete=django.db.models.deletion.CASCADE, + to='fitapp.TimeSeriesDataType'), + ), + migrations.AlterField( + model_name='timeseriesdata', + name='user', + field=models.ForeignKey(help_text="The data's user", + on_delete=django.db.models.deletion.CASCADE, + to=UserModel), + ), + migrations.AlterField( + model_name='timeseriesdata', + name='value', + field=models.CharField(default=None, + help_text='The value of the data. This is typically a number, though saved as a string here. The units can be inferred from the data type. For example, for step data the value might be "9783" (the units) would be "steps"', + max_length=32, null=True), + ), + migrations.AlterField( + model_name='timeseriesdatatype', + name='category', + field=models.IntegerField( + choices=[(0, 'foods'), (1, 'activities'), (2, 'sleep'), (3, 'body')], + help_text='The category of the time series data, one of: 0(foods), 1(activities), 2(sleep), 3(body)'), + ), + migrations.AlterField( + model_name='timeseriesdatatype', + name='resource', + field=models.CharField( + help_text='The specific time series resource. This is the string that will be used for the [resource-path] of the API url referred to in the Fitbit documentation', + max_length=128), + ), + migrations.AlterField( + model_name='userfitbit', + name='access_token', + field=models.TextField(help_text='The OAuth2 access token'), + ), + migrations.AlterField( + model_name='userfitbit', + name='expires_at', + field=models.FloatField( + help_text='The timestamp when the access token expires'), + ), + migrations.AlterField( + model_name='userfitbit', + name='fitbit_user', + field=models.CharField(help_text='The fitbit user ID', max_length=32, + unique=True), + ), + migrations.AlterField( + model_name='userfitbit', + name='refresh_token', + field=models.TextField(help_text='The OAuth2 refresh token'), + ), + migrations.AlterField( + model_name='userfitbit', + name='user', + field=models.OneToOneField(help_text='The user', + on_delete=django.db.models.deletion.CASCADE, + to=UserModel), + ), + ] diff --git a/fitapp/migrations/0010_auto_20180710_1846.py b/fitapp/migrations/0010_auto_20180710_1846.py new file mode 100644 index 0000000..4e78c23 --- /dev/null +++ b/fitapp/migrations/0010_auto_20180710_1846.py @@ -0,0 +1,29 @@ +# Generated by Django 2.0.6 on 2018-07-10 18:46 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + UserModel = getattr(settings, 'FITAPP_USER_MODEL', 'auth.User') + dependencies = [ + migrations.swappable_dependency(UserModel), + ('fitapp', '0009_auto_20180110_1605'), + ] + + operations = [ + migrations.AddField( + model_name='timeseriesdata', + name='intraday', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='timeseriesdata', + name='date', + field=models.DateTimeField(help_text='The date the data was recorded, and time if intraday.'), + ), + migrations.AlterUniqueTogether( + name='timeseriesdata', + unique_together={('user', 'resource_type', 'date', 'intraday')}, + ), + ] diff --git a/fitapp/migrations/0011_userfitbit_last_intraday_step_data_datetime.py b/fitapp/migrations/0011_userfitbit_last_intraday_step_data_datetime.py new file mode 100644 index 0000000..0f1f856 --- /dev/null +++ b/fitapp/migrations/0011_userfitbit_last_intraday_step_data_datetime.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2018-07-12 11:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fitapp', '0010_auto_20180710_1846'), + ] + + operations = [ + migrations.AddField( + model_name='userfitbit', + name='last_intraday_step_data_datetime', + field=models.DateTimeField(default=None), + ), + ] diff --git a/fitapp/migrations/0012_auto_20180712_1132.py b/fitapp/migrations/0012_auto_20180712_1132.py new file mode 100644 index 0000000..8eba93b --- /dev/null +++ b/fitapp/migrations/0012_auto_20180712_1132.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.6 on 2018-07-12 11:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fitapp', '0011_userfitbit_last_intraday_step_data_datetime'), + ] + + operations = [ + migrations.AlterField( + model_name='userfitbit', + name='last_intraday_step_data_datetime', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/fitapp/models.py b/fitapp/models.py index ad81bc7..ddb888f 100644 --- a/fitapp/models.py +++ b/fitapp/models.py @@ -2,15 +2,17 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible - -UserModel = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') +UserModel = getattr(settings, 'FITAPP_USER_MODEL', 'auth.User') @python_2_unicode_compatible class UserFitbit(models.Model): """ A user's fitbit credentials, allowing API access """ user = models.OneToOneField( - UserModel, help_text='The user') + UserModel, + help_text='The user', + on_delete=models.CASCADE + ) fitbit_user = models.CharField( max_length=32, unique=True, help_text='The fitbit user ID') access_token = models.TextField(help_text='The OAuth2 access token') @@ -18,6 +20,14 @@ class UserFitbit(models.Model): expires_at = models.FloatField( help_text='The timestamp when the access token expires') + # Essentially a MapTrek-specific field. + # If the app collects intraday step data, this field stores the datetime of the last + # piece of data taken from Fitbit, zero or nonzero. + last_intraday_step_data_datetime = models.DateTimeField( + null=True, + blank=True, + default=None) + def __str__(self): return self.user.__str__() @@ -70,6 +80,7 @@ class TimeSeriesDataType(models.Model): 'be used for the [resource-path] of the API url referred to in ' 'the Fitbit documentation' )) + intraday_support = models.BooleanField(default=False) def __str__(self): return self.path() @@ -92,10 +103,17 @@ class TimeSeriesData(models.Model): https://dev.fitbit.com/docs/body/#body-time-series """ - user = models.ForeignKey(UserModel, help_text="The data's user") + user = models.ForeignKey( + UserModel, + help_text="The data's user", + on_delete=models.CASCADE + ) resource_type = models.ForeignKey( - TimeSeriesDataType, help_text='The type of time series data') - date = models.DateField(help_text='The date the data was recorded') + TimeSeriesDataType, + help_text='The type of time series data', + on_delete=models.CASCADE + ) + date = models.DateTimeField(help_text='The date the data was recorded, and time if intraday.') value = models.CharField( null=True, default=None, @@ -106,9 +124,10 @@ class TimeSeriesData(models.Model): 'For example, for step data the value might be "9783" (the units) ' 'would be "steps"' )) + intraday = models.BooleanField(default=False) class Meta: - unique_together = ('user', 'resource_type', 'date') + unique_together = ('user', 'resource_type', 'date', 'intraday') def string_date(self): return self.date.strftime('%Y-%m-%d') diff --git a/fitapp/tasks.py b/fitapp/tasks.py index 6a844d7..51dc2fd 100644 --- a/fitapp/tasks.py +++ b/fitapp/tasks.py @@ -1,19 +1,22 @@ import logging import random +import sys +import datetime +import time from celery import shared_task from celery.exceptions import Ignore, Reject from dateutil import parser from django.core.cache import cache from django.db import transaction +from django.utils.timezone import utc from fitbit.exceptions import HTTPBadRequest, HTTPTooManyRequests from . import utils -from .models import UserFitbit, TimeSeriesData, TimeSeriesDataType - +from .models import TimeSeriesData, TimeSeriesDataType, UserFitbit logger = logging.getLogger(__name__) -LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes +LOCK_EXPIRE = 60 * 5 # Lock expires in 5 minutes @shared_task @@ -58,17 +61,17 @@ def get_time_series_data(self, fitbit_user, cat, resource, date=None): resource, cat)) raise Reject(e, requeue=False) - # Create a lock so we don't try to run the same task multiple times - sdat = date.strftime('%Y-%m-%d') if date else 'ALL' - lock_id = '{0}-lock-{1}-{2}-{3}'.format(__name__, fitbit_user, _type, sdat) - if not cache.add(lock_id, 'true', LOCK_EXPIRE): - logger.debug('Already retrieving %s data for date %s, user %s' % ( - _type, fitbit_user, sdat)) - raise Ignore() + # # Create a lock so we don't try to run the same task multiple times + # sdat = date.strftime('%Y-%m-%d') if date else 'ALL' + # lock_id = '{0}-lock-{1}-{2}-{3}'.format(__name__, fitbit_user, _type, sdat) + # if not cache.add(lock_id, 'true', LOCK_EXPIRE): + # logger.debug('Already retrieving %s data for date %s, user %s' % ( + # _type, fitbit_user, sdat)) + # raise Ignore() try: with transaction.atomic(): - # Block until we have exclusive update access to this UserFitbit, so + # Block until we have exclusive up date access to this UserFitbit, so # that another process cannot step on us when we update tokens fbusers = UserFitbit.objects.select_for_update().filter( fitbit_user=fitbit_user) @@ -107,3 +110,79 @@ def get_time_series_data(self, fitbit_user, cat, resource, date=None): except Exception as e: logger.exception("Exception updating data: %s" % e) raise Reject(e, requeue=False) + + +@shared_task(bind=True) +def get_intraday_data(self, fitbit_user, cat, resource, date, tz_offset, start_time=None, end_time=None): + """ + Get the user's intraday data for a specified date, convert to UTC prior to + saving. + + The Fitbit API stipulates that intraday data can only be retrieved for one + day at a time. + """ + try: + _type = TimeSeriesDataType.objects.get(category=cat, resource=resource) + except TimeSeriesDataType.DoesNotExist: + logger.exception("The resource %s in category %s doesn't exist" % + (resource, cat)) + raise Reject(sys.exc_info()[1], requeue=False) + if not _type.intraday_support: + logger.exception("The resource %s in category %s does not support " + "intraday time series" % (resource, cat)) + raise Reject(sys.exc_info()[1], requeue=False) + + # Create a lock so we don't try to run the same task multiple times + # sdat = date.strftime('%Y-%m-%d') + + fbusers = UserFitbit.objects.filter(fitbit_user=fitbit_user) + dates = {'base_date': date, 'period': '1d'} + try: + with transaction.atomic(): + for fbuser in fbusers: + data = utils.get_fitbit_data(fbuser, _type, start_time=start_time, + end_time=end_time, + **dates) + resource_path = _type.path().replace('/', '-') + key = resource_path + "-intraday" + if data[key]['datasetType'] != 'minute': + logger.exception("The resource returned is not " + "minute-level data") + raise Reject(sys.exc_info()[1], requeue=False) + intraday = data[key]['dataset'] + logger.info("Date for intraday task: {}".format(date)) + for minute in intraday: + date_time = parser.parse(date[:10] + ' ' + minute['time']) + utc_datetime = date_time + datetime.timedelta(hours=tz_offset) + utc_datetime = utc_datetime.replace(tzinfo=utc) + value = minute['value'] + # Don't create unnecessary records + if not utils.get_setting('FITAPP_SAVE_INTRADAY_ZERO_VALUES'): + if int(float(value)) == 0: + continue + # Create new record or update existing + tsd, created = TimeSeriesData.objects.get_or_create( + user=fbuser.user, resource_type=_type, date=utc_datetime, + intraday=True) + tsd.value = value + tsd.save() + # Release the lock + except HTTPTooManyRequests: + # We have hit the rate limit for the user, retry when it's reset, + # according to the reply from the failing API call + e = sys.exc_info()[1] + logger.debug('Rate limit reached for user %s, will try again in %s seconds' % + (fitbit_user, e.retry_after_secs)) + raise get_intraday_data.retry(exc=e, countdown=e.retry_after_secs) + except HTTPBadRequest: + # If the resource is elevation or floors, we are just getting this + # error because the data doesn't exist for this user, so we can ignore + # the error + if not ('elevation' in resource or 'floors' in resource): + exc = sys.exc_info()[1] + logger.exception("Exception updating intraday data for user %s: %s" % (fitbit_user, exc)) + raise Reject(exc, requeue=False) + except Exception: + exc = sys.exc_info()[1] + logger.exception("Exception updating data for user %s: %s" % (fitbit_user, exc)) + raise Reject(exc, requeue=False) diff --git a/fitapp/templates/fitapp/error.html b/fitapp/templates/fitapp/error.html index d97770f..47ec86f 100644 --- a/fitapp/templates/fitapp/error.html +++ b/fitapp/templates/fitapp/error.html @@ -1,11 +1,11 @@ - - Fitbit Authentication Error - - -

Fitbit Authentication Error

+ + Fitbit Authentication Error + + +

Fitbit Authentication Error

-

We encountered an error while attempting to authenticate you - through Fitbit.

- +

We encountered an error while attempting to authenticate you + through Fitbit.

+ diff --git a/fitapp/templatetags/fitbit.py b/fitapp/templatetags/fitbit.py index 5ba33be..0bae208 100644 --- a/fitapp/templatetags/fitbit.py +++ b/fitapp/templatetags/fitbit.py @@ -2,8 +2,7 @@ from fitapp import utils - -register= template.Library() +register = template.Library() @register.filter diff --git a/fitapp/tests/__init__.py b/fitapp/tests/__init__.py index 8aacae0..54ad147 100644 --- a/fitapp/tests/__init__.py +++ b/fitapp/tests/__init__.py @@ -1,8 +1,7 @@ from __future__ import absolute_import -from fitapp.tests.test_retrieval import * from fitapp.tests.test_integration import * - +from fitapp.tests.test_retrieval import * # This will make sure the app is always imported when # Django starts so that shared_task will use this app. from .celery import app diff --git a/fitapp/tests/base.py b/fitapp/tests/base.py index 204f9d2..855fa99 100644 --- a/fitapp/tests/base.py +++ b/fitapp/tests/base.py @@ -1,7 +1,9 @@ -from mock import MagicMock, Mock, patch -import django import random import time + +import django +from mock import MagicMock, Mock, patch + try: from urllib.parse import urlencode from string import ascii_letters @@ -11,7 +13,7 @@ from string import letters as ascii_letters from django.contrib.auth.models import User -from django.core.urlresolvers import reverse +from django.urls import reverse from django.test import TestCase from fitbit.api import Fitbit diff --git a/fitapp/tests/celery.py b/fitapp/tests/celery.py index 4e6a487..92b3782 100644 --- a/fitapp/tests/celery.py +++ b/fitapp/tests/celery.py @@ -5,7 +5,6 @@ from celery import Celery from django.conf import settings - # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fitapp.defaults') diff --git a/fitapp/tests/test_commands.py b/fitapp/tests/test_commands.py index 5750088..9b9eef1 100644 --- a/fitapp/tests/test_commands.py +++ b/fitapp/tests/test_commands.py @@ -1,16 +1,12 @@ import json -import requests_mock import time +import requests_mock from django.core import management from django.utils.six import StringIO from fitbit.api import FitbitOauth2Client -from mock import patch -from requests_oauthlib import OAuth2Session from fitapp.models import UserFitbit -from fitapp.management.commands import refresh_tokens - from .base import FitappTestBase diff --git a/fitapp/tests/test_integration.py b/fitapp/tests/test_integration.py index 42fc04e..f73010c 100644 --- a/fitapp/tests/test_integration.py +++ b/fitapp/tests/test_integration.py @@ -1,16 +1,15 @@ import json -import requests_mock import time - from collections import OrderedDict from datetime import datetime +import requests_mock from django.conf import settings from django.contrib import messages from django.contrib.auth.models import AnonymousUser -from django.core.urlresolvers import reverse from django.http import HttpRequest from django.test.utils import override_settings +from django.urls import reverse from fitbit.exceptions import HTTPConflict from freezegun import freeze_time from mock import patch @@ -18,9 +17,8 @@ from fitapp import utils from fitapp.decorators import fitbit_integration_warning -from fitapp.models import UserFitbit, TimeSeriesDataType +from fitapp.models import TimeSeriesDataType, UserFitbit from fitapp.tasks import subscribe, unsubscribe - from .base import FitappTestBase @@ -468,14 +466,14 @@ class TestSubscription(FitappTestBase): @patch('fitbit.Fitbit.subscription') def test_subscribe(self, subscription): subscribe.apply_async((self.fbuser.fitbit_user, 1,)) - subscription.assert_called_once_with(self.user.id, 1,) + subscription.assert_called_once_with(self.user.id, 1, ) @patch('fitbit.Fitbit.subscription') def test_subscribe_error(self, subscription): subscription.side_effect = HTTPConflict apply_result = subscribe.apply_async((self.fbuser.fitbit_user, 1,)) self.assertEqual(apply_result.status, 'REJECTED') - subscription.assert_called_once_with(self.user.id, 1,) + subscription.assert_called_once_with(self.user.id, 1, ) @patch('fitbit.Fitbit.subscription') @patch('fitbit.Fitbit.list_subscriptions') diff --git a/fitapp/tests/test_models.py b/fitapp/tests/test_models.py index c4f89a2..0f31380 100644 --- a/fitapp/tests/test_models.py +++ b/fitapp/tests/test_models.py @@ -1,6 +1,6 @@ -from fitapp.models import TimeSeriesDataType from django.db import IntegrityError +from fitapp.models import TimeSeriesDataType from .base import FitappTestBase diff --git a/fitapp/tests/test_retrieval.py b/fitapp/tests/test_retrieval.py index 0b77856..19c7a97 100644 --- a/fitapp/tests/test_retrieval.py +++ b/fitapp/tests/test_retrieval.py @@ -1,25 +1,24 @@ from __future__ import absolute_import -import celery import json import sys import time - from collections import OrderedDict + +import celery from dateutil import parser from django.core.cache import cache from django.core.files.uploadedfile import InMemoryUploadedFile -from django.core.urlresolvers import reverse from django.test.utils import override_settings +from django.urls import reverse +from fitbit import exceptions as fitbit_exceptions +from fitbit.api import Fitbit from freezegun import freeze_time from mock import MagicMock, patch from requests_oauthlib import OAuth2Session -from fitbit import exceptions as fitbit_exceptions -from fitbit.api import Fitbit, FitbitOauth2Client - from fitapp import utils -from fitapp.models import UserFitbit, TimeSeriesData, TimeSeriesDataType +from fitapp.models import TimeSeriesData, TimeSeriesDataType, UserFitbit from fitapp.tasks import get_time_series_data try: @@ -175,7 +174,7 @@ def test_subscription_update(self, get_fitbit_data): self.assertEqual( cache.get('fitapp.get_time_series_data-lock-%s-%s-%s' % ( category, resource.resource, self.date) - ), None) + ), None) date = parser.parse(self.date) for tsd in TimeSeriesData.objects.filter(user=self.user, date=date): assert tsd.value == self.value @@ -194,7 +193,7 @@ def test_subscription_update_file(self, get_fitbit_data): self.assertEqual( cache.get('fitapp.get_time_series_data-lock-%s-%s-%s' % ( category, resource.resource, self.date) - ), None) + ), None) date = parser.parse(self.date) for tsd in TimeSeriesData.objects.filter(user=self.user, date=date): assert tsd.value == self.value @@ -289,6 +288,7 @@ def test_subscription_update_too_many(self, get_fitbit_data): self.fbuser.fitbit_user, _type, self.date) exc = fitbit_exceptions.HTTPTooManyRequests(self._error_response()) exc.retry_after_secs = 21 + def side_effect(*args, **kwargs): # Delete the cache lock after the first try and adjust the # get_fitbit_data mock to be successful @@ -299,6 +299,7 @@ def side_effect(*args, **kwargs): 'value': '34' }] raise exc + get_fitbit_data.side_effect = side_effect category = getattr(TimeSeriesDataType, self.category) resources = TimeSeriesDataType.objects.filter(category=category) @@ -306,7 +307,7 @@ def side_effect(*args, **kwargs): result = get_time_series_data.apply_async( (self.fbuser.fitbit_user, _type.category, _type.resource,), {'date': parser.parse(self.date)}) - result.get() + # result.get(disable_sync_subtasks=False) # Since celery is in eager mode, we expect a Retry exception first # and then a second task execution that is successful self.assertEqual(get_fitbit_data.call_count, 2) @@ -402,6 +403,25 @@ def test_problem_queueing_task(self): except: assert False, 'Any errors should be captured in the view' +""" + def test_retrieve_intraday_data(self): + print(utils.get_setting('FITAPP_GET_INTRADAY')) + + steps_tsdt = TimeSeriesDataType.objects.get(resource='steps') + steps_tsdt.intraday_support = True + steps_tsdt.save() + + subscription_update_data = json.dumps({'body':[{ + "collectionType": "activities", + "date": "2010-03-01", + "ownerId": "228S74", + "ownerType": "user", + "subscriptionId": "1234" + }]}) + + self.client.post('/update/', subscription_update_data) +""" + class RetrievalViewTestBase(object): """Base methods for the get_steps view.""" diff --git a/fitapp/tests/test_verification.py b/fitapp/tests/test_verification.py index 930a5f7..104ec81 100644 --- a/fitapp/tests/test_verification.py +++ b/fitapp/tests/test_verification.py @@ -3,9 +3,9 @@ https://dev.fitbit.com/docs/subscriptions/#verify-a-subscriber """ -from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings +from django.urls import reverse class TestVerification(TestCase): diff --git a/fitapp/urls.py b/fitapp/urls.py index 0476c6c..c29b93f 100644 --- a/fitapp/urls.py +++ b/fitapp/urls.py @@ -2,7 +2,6 @@ from . import views - urlpatterns = [ # OAuth authentication url(r'^login/$', views.login, name='fitbit-login'), diff --git a/fitapp/utils.py b/fitapp/utils.py index 80c79e3..11f2111 100644 --- a/fitapp/utils.py +++ b/fitapp/utils.py @@ -1,10 +1,9 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured - from fitbit import Fitbit from . import defaults -from .models import UserFitbit, TimeSeriesDataType +from .models import TimeSeriesDataType, UserFitbit def create_fitbit(consumer_key=None, consumer_secret=None, **kwargs): @@ -35,7 +34,7 @@ def is_integrated(user): :param user: A Django User. """ - if user.is_authenticated() and user.is_active: + if user.is_authenticated and user.is_active: return UserFitbit.objects.filter(user=user).exists() return False @@ -46,7 +45,7 @@ def get_valid_periods(): def get_fitbit_data(fbuser, resource_type, base_date=None, period=None, - end_date=None): + end_date=None, start_time=None, end_time=None): """Creates a Fitbit API instance and retrieves step data for the period. Several exceptions may be thrown: @@ -64,10 +63,16 @@ def get_fitbit_data(fbuser, resource_type, base_date=None, period=None, """ fb = create_fitbit(**fbuser.get_user_data()) resource_path = resource_type.path() - data = fb.time_series(resource_path, user_id=fbuser.fitbit_user, - period=period, base_date=base_date, - end_date=end_date) - return data[resource_path.replace('/', '-')] + + if get_setting('FITAPP_GET_INTRADAY') and resource_type.intraday_support: + base_date = base_date[:10] + data = fb.intraday_time_series(resource_path, base_date=base_date, + start_time=start_time, end_time=end_time) + else: + data = fb.time_series(resource_path, user_id=fbuser.fitbit_user, + period=period, base_date=base_date, + end_date=end_date) + return data def get_setting(name, use_defaults=True): diff --git a/fitapp/views.py b/fitapp/views.py index 4915a8b..c398325 100644 --- a/fitapp/views.py +++ b/fitapp/views.py @@ -1,30 +1,25 @@ -from functools import cmp_to_key import simplejson as json - from dateutil import parser from dateutil.relativedelta import relativedelta -from django.contrib.auth.decorators import login_required from django.contrib.auth.signals import user_logged_in from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import reverse from django.dispatch import receiver -from django.http import HttpResponse, HttpResponseServerError, Http404 +from django.http import Http404, HttpResponse, HttpResponseServerError from django.shortcuts import redirect, render +from django.urls import reverse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET +from fitbit.exceptions import ( + HTTPConflict, HTTPForbidden, HTTPServerError, HTTPUnauthorized +) from six import string_types -from fitbit.exceptions import (HTTPUnauthorized, HTTPForbidden, HTTPConflict, - HTTPServerError) - -from . import forms -from . import utils -from .models import UserFitbit, TimeSeriesData, TimeSeriesDataType -from .tasks import get_time_series_data, subscribe, unsubscribe +from . import forms, utils +from .models import TimeSeriesData, TimeSeriesDataType, UserFitbit +from .tasks import get_time_series_data, subscribe, unsubscribe, get_intraday_data -@login_required def login(request): """ Begins the OAuth authentication process by obtaining a Request Token from @@ -53,7 +48,6 @@ def login(request): return redirect(token_url) -@login_required def complete(request): """ After the user authorizes us, Fitbit sends a callback to this URL to @@ -70,9 +64,15 @@ def complete(request): If :ref:`FITAPP_SUBSCRIBE` is set to True, add a subscription to user data at this time. + Requires the pk of the user or model that will be associated with fitapp + data to be inserted into request.session['fb_user_id'] prior to calling + the view. + URL name: `fitbit-complete` """ + user_model = UserFitbit.user.field.remote_field.model + try: code = request.GET['code'] except KeyError: @@ -83,14 +83,12 @@ def complete(request): try: token = fb.client.fetch_access_token(code, callback_uri) access_token = token['access_token'] + fb_user_id = int(request.session.get('fb_user_id')) + user = user_model.objects.get(pk=fb_user_id) fitbit_user = token['user_id'] except KeyError: return redirect(reverse('fitbit-error')) - if UserFitbit.objects.filter(fitbit_user=fitbit_user).exists(): - return redirect(reverse('fitbit-error')) - - user = request.user fbuser, _ = UserFitbit.objects.update_or_create(user=user, defaults={ 'fitbit_user': fitbit_user, 'access_token': access_token, @@ -147,7 +145,7 @@ def complete(request): def create_fitbit_session(sender, request, user, **kwargs): """ If the user is a fitbit user, update the profile in the session. """ - if user.is_authenticated() and utils.is_integrated(user) and \ + if user.is_authenticated and utils.is_integrated(user) and \ user.is_active: fbuser = UserFitbit.objects.filter(user=user) if fbuser.exists(): @@ -158,7 +156,6 @@ def create_fitbit_session(sender, request, user, **kwargs): pass -@login_required def error(request): """ The user is redirected to this view if we encounter an error acquiring @@ -184,7 +181,6 @@ def error(request): return render(request, utils.get_setting('FITAPP_ERROR_TEMPLATE'), {}) -@login_required def logout(request): """Forget this user's Fitbit credentials. @@ -267,12 +263,18 @@ def update(request): key=lambda tsdt: res_list.index(tsdt.resource) ) for i, _type in enumerate(tsdts): + if utils.get_setting('FITAPP_GET_INTRADAY') and _type.intraday_support: + date = parser.parse(update['date']) + get_intraday_data.apply_async( + (update['ownerId'], _type.category, _type.resource, date, 0), + countdown=(btw_delay * i)) # Offset each call by a few seconds so they don't bog down # the server - get_time_series_data.apply_async( - (update['ownerId'], _type.category, _type.resource,), - {'date': parser.parse(update['date'])}, - countdown=(btw_delay * i)) + else: + get_time_series_data.apply_async( + (update['ownerId'], _type.category, _type.resource,), + {'date': parser.parse(update['date'])}, + countdown=(btw_delay * i)) except (KeyError, ValueError, OverflowError): raise Http404 except ImproperlyConfigured as e: @@ -421,7 +423,7 @@ def get_data(request, category, resource): return make_response(104) fitapp_subscribe = utils.get_setting('FITAPP_SUBSCRIBE') - if not user.is_authenticated() or not user.is_active: + if not user.is_authenticated or not user.is_active: return make_response(101) if not fitapp_subscribe and not utils.is_integrated(user): return make_response(102) diff --git a/intraday-support-docs.md b/intraday-support-docs.md new file mode 100644 index 0000000..b709380 --- /dev/null +++ b/intraday-support-docs.md @@ -0,0 +1,86 @@ +Summary for 7/12/18 + +I'm still trying to get a handle on what changed between the original django-fitbit and our fork. +Particularly the FITAPP_SUBSCRIPTIONS setting in the original django-fitbit - there are no usages of this in the +old MapTrek, and it defaults to None, which would seem to make some of the code not work, but it does. +The old MapTrek uses a setting called FITAPP_SUBSCRIPTION_COLLECTION to enumerate what categories of data +to subscribe to, and this is used when subscriptions are created but i can't exactly figure out +what its role is in actually getting data. + +Investigating further I found that FITAPP_SUBSCRIPTIONS is not set in the old Maptrek. (set to None) + + +When we get a subscription notification, it looks like this: +{ + "collectionType": "activities", + "date": "2010-03-01", + "ownerId": "184X36", + "ownerType": "user", + "subscriptionId": "2345" + } +Note that it doesn't specify what kind of activity data is available. Since we only care about steps, +we may get activity notifications that end up being useless because they're not for step data. I'm not sure +but this may be a reason behind the dropped data issue. + + +How to get intraday support to work + +- There must be a FITAPP_GET_INTRADAY boolean setting somewhere in the Django project's settings files. +If True, django-fitbit will retrieve and create records for intraday data for any TimeSeriesDataType marked +as intraday-compatible. +- FITAPP_GET_INTRADAY defaults to False. +- When TimeSeriesDataTypes are created, they must be given intraday_support = True + - VERY IMPORTANT: There are not currently checks to see if the given Fitbit app has authorization for + a certain category of intraday data. Be sure your app has authorization before attempting to retrieve it. +- Intraday TimeSeriesData instances will be marked with intraday = True. +- There must be a FITAPP_USER_MODEL setting set to whatever model you want UserFitbits and TimeSeriesData associated with. + + +What was changed +- Add FITAPP_GET_INTRADAY setting to: + - fitapp/defaults.py - False + - test_settings.py - True + +- Changes to fitapp/models.py + - Add intraday_support field to TimeSeriesDataType, defaults to False. + - Add intraday field to TimeSeriesData, defaults to False. + - Add last_intraday_step_data_datetime field to UserFitbit, defaults to None. + - Change date field for TimeSeriesData from DateField to DateTimeField, change help text to reflect this. + - NOTE: I would like to change the name of this field to date_time. + - Change unique_together values for TimeSeriesData by adding 'intraday' as a requirement. + +Changes to fitapp/migrations + - Move addition of TimeSeriesDataType.intraday_support to 0001_initial.py to resolve errors. + - Add intraday_support field to every TimeSeriesDataType in fitapp/fixtures/initial_data.json to resolve errors. + +- Changes to fitapp/views.py + - Change the task scheduling portion of the update view to schedule the get_intraday_data task + when the data needed is intraday enabled, instead of calling get_time_series_data. + +- Changes to fitapp/tasks.py + - Adapt preexisting get_intraday_data task to work with current django-fitbit. + +- Changes to fitapp/utils.py + - Modify get_fitbit_data to use python-fitbit's intraday_time_series function to retrieve data when + a resource category is intraday enabled. + - Add an optional start_time and end_time to get_fitbit_data to use for retrieving intraday data. + +- Changes to enable swappable model to which UserFitbits and data is associated + - Create new setting, FITAPP_USER_MODEL, which must be set to the model to associate UserFitbits and TimeSeriesData to. + - Set this setting to a default in test_settings.py + +Planned changes + +- Add last_intraday_data_from_when or similar field to UserFitbit +- Rewrite get_intraday_data to use appropriate python-fitbit function and to update/maintain last_intraday_data field. + - Also to ask for a time range instead of a full day of data. + +Have fitbit listener view call a different task depending on if data is intraday or not. +Probably need to add fitapp subscriptions value for test settings? + + + + +Unknown number of new settings introduced in fork of django-fitbit +I have yet to figure out how FITAPP_SUBSCRIPTIONS is set in Maptrek. +Fork introduces FITAPP_SUBSCRIPTION_COLLECTION diff --git a/requirements/base.txt b/requirements/base.txt index f402638..24b63ff 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,4 @@ fitbit>=0.3.0 -celery>=3.1.13 -simplejson -six +celery +simplejson>=3.13,<4 +six \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 274bd34..e69de29 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +0,0 @@ --r base.txt --r test.txt - -django-debug-toolbar -django>=1.8,<1.11 -tox>=1.8,<2.4 diff --git a/requirements/test.txt b/requirements/test.txt index a5f6ec8..142a0c8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,5 @@ -coverage>4 -mock>=1,<1.4 -freezegun>=0.2.3,<0.4 -requests-mock>=1.2.0 -Sphinx>=1.2 +coverage +mock>=2,<3 +freezegun>=0.3.9,<0.4 +requests-mock>=1.4.0,<1.5 +Sphinx>=1.6 diff --git a/setup.py b/setup.py index e087580..3276e5a 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ description="Django integration for python-fitbit", long_description=open("README.md").read(), classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Alpha", 'License :: OSI Approved :: Apache Software License', "Environment :: Web Environment", "Framework :: Django", @@ -28,6 +28,7 @@ "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: PyPy" ], diff --git a/test_settings.py b/test_settings.py index f623053..6268a42 100644 --- a/test_settings.py +++ b/test_settings.py @@ -1,5 +1,6 @@ import sys +from django.conf import settings DATABASES = { 'default': { @@ -21,6 +22,8 @@ FITAPP_CONSUMER_SECRET = 'FAKE_CONSUMER_KEY' FITAPP_SUBSCRIBE = True FITAPP_SUBSCRIBER_ID = 1 +FITAPP_GET_INTRADAY = True +FITAPP_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') LOGGING = { 'version': 1, @@ -44,7 +47,7 @@ }, ] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', )