Compare commits
495 Commits
0.16
...
spacecruft
Author | SHA1 | Date |
---|---|---|
Jeff Moe | 5a7751ab30 | |
Jeff Moe | 8fd9db47e5 | |
Jeff Moe | e6a15bf889 | |
Vasilis Tsiligiannis | f6d1948e31 | |
George Sfoungaris | 78c52e7ead | |
Michał Drzał | a94d14ba71 | |
George Sfoungaris | aafb7a4c5e | |
George Sfoungaris | 24aa35d904 | |
George Sfoungaris | f847b783f6 | |
Alfredos-Panagiotis Damkalis | 5eff9c7e13 | |
George Sfoungaris | 8dbea1c47e | |
George Sfoungaris | 4eb3a9cce6 | |
George Sfoungaris | 0e2cfdea83 | |
George Sfoungaris | ae5bbb962d | |
Alfredos-Panagiotis Damkalis | 04b0605e09 | |
Vasilis Tsiligiannis | fb4a514693 | |
Alfredos-Panagiotis Damkalis | 0188846f0f | |
Alfredos-Panagiotis Damkalis | 97dc2b8d79 | |
Alfredos-Panagiotis Damkalis | aa45aee2a3 | |
Alfredos-Panagiotis Damkalis | 4e3e6c5902 | |
Alfredos-Panagiotis Damkalis | 7f1911082f | |
Alfredos-Panagiotis Damkalis | 56099d7055 | |
Alfredos-Panagiotis Damkalis | 03309529a2 | |
Alfredos-Panagiotis Damkalis | a13e2a0392 | |
Alfredos-Panagiotis Damkalis | 3b98e66dc8 | |
Alfredos-Panagiotis Damkalis | d7df35032d | |
Alfredos-Panagiotis Damkalis | 0dc188f41b | |
Alfredos-Panagiotis Damkalis | 9583d20c49 | |
Alfredos-Panagiotis Damkalis | 54327a8763 | |
Alfredos-Panagiotis Damkalis | aecfcda191 | |
Alfredos-Panagiotis Damkalis | 94b2594ee7 | |
Alfredos-Panagiotis Damkalis | b563760219 | |
Alfredos-Panagiotis Damkalis | 8870b425f5 | |
Alfredos-Panagiotis Damkalis | f63ac4294c | |
Alfredos-Panagiotis Damkalis | 3d4afd6f78 | |
Alfredos-Panagiotis Damkalis | 9d08f9d275 | |
Alfredos-Panagiotis Damkalis | 6d5f0d1ba8 | |
Vasilis Tsiligiannis | 13e045164e | |
Alfredos-Panagiotis Damkalis | 96ee6f2448 | |
Vasilis Tsiligiannis | f6829b658a | |
Vasilis Tsiligiannis | 6eda3ca996 | |
Alfredos-Panagiotis Damkalis | a4d6601fb1 | |
Alfredos-Panagiotis Damkalis | 842a6b3d77 | |
Alfredos-Panagiotis Damkalis | 56255a0ad5 | |
Alfredos-Panagiotis Damkalis | cdd2cee02f | |
Alfredos-Panagiotis Damkalis | 4117ef7fb4 | |
Alfredos-Panagiotis Damkalis | 6313b93b67 | |
Alfredos-Panagiotis Damkalis | e5c0f03339 | |
Alfredos-Panagiotis Damkalis | 7ae859baea | |
Alfredos-Panagiotis Damkalis | da58d39357 | |
Alfredos-Panagiotis Damkalis | 5e219954bb | |
Alfredos-Panagiotis Damkalis | e0cddccb85 | |
Alfredos-Panagiotis Damkalis | 26677db242 | |
Alfredos-Panagiotis Damkalis | 09bf23b376 | |
Fabian P. Schmidt | a8370bb06b | |
Alfredos-Panagiotis Damkalis | 2e13954fef | |
Alfredos-Panagiotis Damkalis | 1eb1526cd5 | |
Vasilis Tsiligiannis | ce07c90248 | |
Alfredos-Panagiotis Damkalis | 9ff314fe88 | |
Alfredos-Panagiotis Damkalis | 0f95cdec29 | |
deckbsd | 52d23bc7e0 | |
Fabian P. Schmidt | fffca10916 | |
Fabian P. Schmidt | 9f629bb715 | |
Fabian P. Schmidt | d674f10e70 | |
deckbsd | 51d9a3f499 | |
deckbsd | 8b641a89dc | |
Fabian P. Schmidt | ba950b6a34 | |
deckbsd | 24521a7f98 | |
Fabian P. Schmidt | 49ee559251 | |
Fabian P. Schmidt | 37daecf507 | |
Fabian P. Schmidt | 52147ade28 | |
Vasilis Tsiligiannis | 26c8f4d15f | |
Alfredos-Panagiotis Damkalis | 283cca5e40 | |
Alfredos-Panagiotis Damkalis | a175b59f1c | |
Alfredos-Panagiotis Damkalis | 69f4938908 | |
Alfredos-Panagiotis Damkalis | 84f754dc62 | |
Alfredos-Panagiotis Damkalis | 934a8eeccb | |
Papadeas Pierros | 51ec39a333 | |
Alfredos-Panagiotis Damkalis | 34f8c4e919 | |
Alfredos-Panagiotis Damkalis | 49eb53be30 | |
Alfredos-Panagiotis Damkalis | c8081962ae | |
Alfredos-Panagiotis Damkalis | 6341c26783 | |
Alfredos-Panagiotis Damkalis | 3c1eda6c2e | |
Alfredos-Panagiotis Damkalis | e7b0e56a17 | |
Alfredos-Panagiotis Damkalis | 3391fa8604 | |
Papadeas Pierros | f8ca235faa | |
Alfredos-Panagiotis Damkalis | 0d137863b3 | |
Alfredos-Panagiotis Damkalis | 06e2f032fe | |
Alfredos-Panagiotis Damkalis | 1513e998bb | |
Patrick Dohmen | 2081519af8 | |
Corey Shields | 79ca1834c9 | |
Vasilis Tsiligiannis | 33357e9207 | |
Alfredos-Panagiotis Damkalis | f046863123 | |
Corey Shields | 37b83bfa66 | |
Alfredos-Panagiotis Damkalis | d25436d594 | |
Alfredos-Panagiotis Damkalis | 804b69e442 | |
Corey Shields | 4141e275cc | |
Alfredos-Panagiotis Damkalis | 286c744141 | |
Corey Shields | 76c0d8f7b9 | |
Corey Shields | ad9f4f0d6f | |
Corey Shields | 292f1978db | |
Corey Shields | cbb169e5de | |
Corey Shields | c217f66b2d | |
Corey Shields | 33dd436cc1 | |
Alfredos-Panagiotis Damkalis | 3b05184d14 | |
Alfredos-Panagiotis Damkalis | 2f11e6b8e8 | |
Alfredos-Panagiotis Damkalis | 418fe5ab3d | |
Alfredos-Panagiotis Damkalis | c75b491b9b | |
Alfredos-Panagiotis Damkalis | f4ea975e0c | |
Alfredos-Panagiotis Damkalis | af2d04a33f | |
Alfredos-Panagiotis Damkalis | d1eab762fa | |
Alfredos-Panagiotis Damkalis | 0d81ecbe77 | |
Alfredos-Panagiotis Damkalis | ce6a5ebf4c | |
Vasilis Tsiligiannis | 79ebbe219e | |
Corey Shields | 967c05b212 | |
Alfredos-Panagiotis Damkalis | cc073daf45 | |
Alfredos-Panagiotis Damkalis | 29c0d3654b | |
Alfredos-Panagiotis Damkalis | 4ea55fa1cc | |
Alfredos-Panagiotis Damkalis | f6fd93f53e | |
Alfredos-Panagiotis Damkalis | 5426167d5b | |
Alfredos-Panagiotis Damkalis | a97f9666db | |
Alfredos-Panagiotis Damkalis | 2805acde27 | |
Alfredos-Panagiotis Damkalis | 00a91ba947 | |
Alfredos-Panagiotis Damkalis | efd5dd1a96 | |
Corey Shields | 2b17d32a67 | |
Alfredos-Panagiotis Damkalis | 8adda93204 | |
Alfredos-Panagiotis Damkalis | afda478ccc | |
Alfredos-Panagiotis Damkalis | 443668b90c | |
Alfredos-Panagiotis Damkalis | 2b3282792d | |
Alfredos-Panagiotis Damkalis | 8ca9004fc9 | |
Alfredos-Panagiotis Damkalis | 0e6c120cc9 | |
Alfredos-Panagiotis Damkalis | 4aed383ed0 | |
Alfredos-Panagiotis Damkalis | 87ac2f9dc9 | |
Alfredos-Panagiotis Damkalis | 4b8239739a | |
Alfredos-Panagiotis Damkalis | 1b366d1722 | |
Alfredos-Panagiotis Damkalis | f1d885f172 | |
Corey Shields | 131831a811 | |
Corey Shields | bb24e771d6 | |
Vasilis Tsiligiannis | 0f09f58629 | |
Vasilis Tsiligiannis | fd337e9dd0 | |
Vasilis Tsiligiannis | 8107ee4460 | |
Vasilis Tsiligiannis | 98acbd2640 | |
Vasilis Tsiligiannis | 79382a5c2f | |
Vasilis Tsiligiannis | d92e655cb5 | |
Vasilis Tsiligiannis | 648db98037 | |
Vasilis Tsiligiannis | a114826f80 | |
Vasilis Tsiligiannis | 3d96fe1e41 | |
Corey Shields | 5e03f7c759 | |
Corey Shields | bec7469dc2 | |
Corey Shields | 8ec3b93aba | |
Corey Shields | e5ddc38e96 | |
Corey Shields | d153ece5ef | |
Corey Shields | bcee356c24 | |
Corey Shields | 46eb5aa8c8 | |
Corey Shields | 17f4ab100b | |
Corey Shields | a1aafce76b | |
Corey Shields | 0fc7107172 | |
Corey Shields | 46883750e4 | |
Vasilis Tsiligiannis | 18ff90735e | |
Corey Shields | 83f9b07e21 | |
Vasilis Tsiligiannis | cc422353e7 | |
Vasilis Tsiligiannis | 1abbf425ef | |
Vasilis Tsiligiannis | 1da488642a | |
Vasilis Tsiligiannis | 7629b8f762 | |
Vasilis Tsiligiannis | 9abde9b765 | |
Vasilis Tsiligiannis | 99f3a5fcc2 | |
deckbsd | c07696c80e | |
Papadeas Pierros | 9e8e1b169f | |
Vasilis Tsiligiannis | 8d60968729 | |
deckbsd | 3ccc3ef0fd | |
Corey Shields | c7772f4c78 | |
Vasilis Tsiligiannis | a3866f180b | |
Vasilis Tsiligiannis | fce52ec637 | |
Vasilis Tsiligiannis | 1f2dc4b75f | |
Vasilis Tsiligiannis | 6680a7fe12 | |
Vasilis Tsiligiannis | 6da1afd742 | |
Vasilis Tsiligiannis | b98cb87f4d | |
Vasilis Tsiligiannis | d435eef416 | |
Vasilis Tsiligiannis | 816eba90c2 | |
Patrick Dohmen | 83f383d851 | |
Corey Shields | b44b618c04 | |
Corey Shields | 2400c6b4f6 | |
Corey Shields | a653f0697c | |
Vasilis Tsiligiannis | 08cd629c05 | |
Fabian P. Schmidt | 3ed0d750db | |
Pierros Papadeas | 2835df9553 | |
Alfredos-Panagiotis Damkalis | 8e52ab8e8d | |
Vasilis Tsiligiannis | f85273a592 | |
Alfredos-Panagiotis Damkalis | f53d240f5b | |
Alfredos-Panagiotis Damkalis | 387affac12 | |
Alfredos-Panagiotis Damkalis | 2b594d427c | |
Alfredos-Panagiotis Damkalis | b31d40809f | |
Alfredos-Panagiotis Damkalis | ed0a22503d | |
Vasilis Tsiligiannis | 5406261482 | |
Vasilis Tsiligiannis | 8561037130 | |
Vasilis Tsiligiannis | 34c7e4fd03 | |
Alfredos-Panagiotis Damkalis | 815d56a6f8 | |
Vasilis Tsiligiannis | e29e8eb5a4 | |
Vasilis Tsiligiannis | 30e0c27461 | |
Vasilis Tsiligiannis | e6a76cf1a7 | |
Vasilis Tsiligiannis | 13c39be360 | |
Vasilis Tsiligiannis | b0aebd41b9 | |
Vasilis Tsiligiannis | 9a39a70722 | |
Vasilis Tsiligiannis | 53ea7728e2 | |
Vasilis Tsiligiannis | 6af7d06a74 | |
Vasilis Tsiligiannis | b457e7460d | |
Patrick Dohmen | cb0b00e304 | |
Alfredos-Panagiotis Damkalis | 8e2f52a59f | |
Patrick Dohmen | 1e9b695569 | |
Alfredos-Panagiotis Damkalis | 09e630890f | |
Alfredos-Panagiotis Damkalis | a841bd48d5 | |
Alfredos-Panagiotis Damkalis | 63e1c50f8d | |
Alfredos-Panagiotis Damkalis | e38fa63079 | |
Alfredos-Panagiotis Damkalis | 87d5329c36 | |
Patrick Dohmen | 6dd9bff360 | |
Patrick Dohmen | 2ef400acf9 | |
Vasilis Tsiligiannis | 31320f739c | |
Vasilis Tsiligiannis | 2b714a754e | |
Alfredos-Panagiotis Damkalis | 6a12f76a26 | |
Alfredos-Panagiotis Damkalis | ae48ac7008 | |
Alfredos-Panagiotis Damkalis | 74fd41b2a8 | |
Alfredos-Panagiotis Damkalis | e27fbb09b9 | |
Pierros Papadeas | 5c01c5d72b | |
Vasilis Tsiligiannis | b51e6f4792 | |
Vasilis Tsiligiannis | 498fa704a3 | |
Vasilis Tsiligiannis | e2086c72b8 | |
Alfredos-Panagiotis Damkalis | 45eb948f57 | |
Alfredos-Panagiotis Damkalis | 816ab8cd49 | |
Alfredos-Panagiotis Damkalis | 09bb5b0875 | |
Alfredos-Panagiotis Damkalis | dd10892eaf | |
Vasilis Tsiligiannis | 37cc41d86c | |
Corey Shields | 0dbb1df42d | |
Corey Shields | 2e25763185 | |
Corey Shields | b5dd2eb4ea | |
Corey Shields | 3af17917ba | |
Corey Shields | eb859b7a44 | |
Pierros Papadeas | fa32a0ebb4 | |
Corey Shields | d6f6ff0c2e | |
Vasilis Tsiligiannis | cb705d3c1b | |
Vasilis Tsiligiannis | 613ef7cdbb | |
Vasilis Tsiligiannis | db9457776c | |
Corey Shields | 3d1c504e1d | |
Corey Shields | 402ec942ab | |
Corey Shields | 0600d2a090 | |
Corey Shields | b002ca6e37 | |
Corey Shields | b7e82848d5 | |
Corey Shields | 5395122ac7 | |
Corey Shields | aa5eaaad74 | |
Pierros Papadeas | 87ab59c738 | |
Alfredos-Panagiotis Damkalis | f18140e153 | |
Alfredos-Panagiotis Damkalis | e1b859887f | |
Alfredos-Panagiotis Damkalis | e2b916f3ed | |
Alfredos-Panagiotis Damkalis | ce1c189f02 | |
Corey Shields | fda7defc8b | |
Alfredos-Panagiotis Damkalis | 00e9cb1cc3 | |
Corey Shields | 75ad7ecd35 | |
Alfredos-Panagiotis Damkalis | 2c97ff4f58 | |
Alfredos-Panagiotis Damkalis | 41cb47f9ff | |
Corey Shields | 771aa22ecd | |
Corey Shields | 931e0893d5 | |
Corey Shields | 567df49a8f | |
Corey Shields | 46c73722c4 | |
Corey Shields | 07a4904ff8 | |
Pierros Papadeas | 1fd8406ab5 | |
Pierros Papadeas | 01c581ea8a | |
Alfredos-Panagiotis Damkalis | ed6ac366fb | |
Corey Shields | f6d2bcdc93 | |
Corey Shields | 44daf627be | |
Corey Shields | 8b90fd1ff9 | |
Corey Shields | f05658476f | |
Corey Shields | 3149c91e56 | |
Corey Shields | b198e188d3 | |
Pierros Papadeas | 9ad52c8da8 | |
Corey Shields | 488fd9d7b4 | |
Corey Shields | 5d0df9ac8f | |
Corey Shields | 2f51df5337 | |
Pierros Papadeas | 657260ed9f | |
deckbsd | 72b1a4abad | |
Pierros Papadeas | 86d1bd6abc | |
Pierros Papadeas | 96e971d614 | |
deckbsd | 01d9602693 | |
Alfredos-Panagiotis Damkalis | 7938239120 | |
Corey Shields | 0ba46f0913 | |
Corey Shields | 2b80a3a88f | |
Corey Shields | b41b77be94 | |
deckbsd | 768779ec6a | |
deckbsd | ee7c8fdcef | |
Pierros Papadeas | c444bf3645 | |
deckbsd | 461c8af026 | |
Corey Shields | c1d57a79cb | |
Corey Shields | 95e89dc4b5 | |
Corey Shields | 508a22acf0 | |
Pierros Papadeas | c17c7104f8 | |
Pierros Papadeas | a1fe08f352 | |
Corey Shields | ff3d88001e | |
deckbsd | faa38e3960 | |
deckbsd | f9baf45aa1 | |
Pierros Papadeas | 1f6f74be64 | |
deckbsd | ead89b6d03 | |
deckbsd | f0bee6eda9 | |
deckbsd | 60f2356c73 | |
Corey Shields | b7d2392c35 | |
Corey Shields | 4c3147d4c5 | |
deckbsd | 05d4f8a705 | |
Corey Shields | cbd0ec096a | |
Corey Shields | 122baae403 | |
Corey Shields | 68e1e20a4e | |
deckbsd | 999bab50c6 | |
Corey Shields | a7141c5b30 | |
deckbsd | 63a93fa1ed | |
deckbsd | 5122729752 | |
deckbsd | 317eed5a34 | |
Alfredos-Panagiotis Damkalis | 8c319bfe62 | |
Alfredos-Panagiotis Damkalis | f5e61c0124 | |
Alfredos-Panagiotis Damkalis | 504392d1e7 | |
Alfredos-Panagiotis Damkalis | 6094847432 | |
Alfredos-Panagiotis Damkalis | aef8d5c33b | |
Alfredos-Panagiotis Damkalis | 118cb7cadc | |
Alfredos-Panagiotis Damkalis | 39befe0dfc | |
Pierros Papadeas | 980ef8abe2 | |
Pierros Papadeas | a0d398f129 | |
Pierros Papadeas | ef1e315984 | |
Alfredos-Panagiotis Damkalis | 67f229d2ed | |
Alfredos-Panagiotis Damkalis | 20153300cf | |
Alfredos-Panagiotis Damkalis | 412c2aa085 | |
Pierros Papadeas | c98aea69fc | |
deckbsd | ef1c4313aa | |
Pierros Papadeas | d8aef58025 | |
Alfredos-Panagiotis Damkalis | 4891efd75e | |
Alfredos-Panagiotis Damkalis | 6a81937465 | |
Alfredos-Panagiotis Damkalis | 90d860377c | |
Alfredos-Panagiotis Damkalis | 1736abf6b2 | |
Pierros Papadeas | 9f76d3cb25 | |
Vasilis Tsiligiannis | b7d2d4ff95 | |
Vasilis Tsiligiannis | ddf62bf660 | |
Alfredos-Panagiotis Damkalis | fee2c8683e | |
Vasilis Tsiligiannis | 79efc9a1c3 | |
Vasilis Tsiligiannis | 2e59bc3acc | |
Alfredos-Panagiotis Damkalis | a088e5d03b | |
Alfredos-Panagiotis Damkalis | 4ba7b8cfa9 | |
Alfredos-Panagiotis Damkalis | 1f0f0767f9 | |
deckbsd | 1d1c72edf4 | |
Vasilis Tsiligiannis | d77e9b0f86 | |
Vasilis Tsiligiannis | e4b2161f82 | |
Vasilis Tsiligiannis | b9b1783434 | |
Vasilis Tsiligiannis | c06fe77cc1 | |
Vasilis Tsiligiannis | e9334d89e1 | |
Alfredos-Panagiotis Damkalis | 3b2f41bded | |
Vasilis Tsiligiannis | 1664f68068 | |
Vasilis Tsiligiannis | f656347aed | |
Vasilis Tsiligiannis | b74d8ba581 | |
Vasilis Tsiligiannis | 7d862342f1 | |
Vasilis Tsiligiannis | 80a5c561f1 | |
Vasilis Tsiligiannis | 4435d64a8d | |
Alfredos-Panagiotis Damkalis | fcf1e0852e | |
Vasilis Tsiligiannis | a7af3b8c69 | |
Vasilis Tsiligiannis | b60e7182d1 | |
Vasilis Tsiligiannis | 091c61dd65 | |
Alfredos-Panagiotis Damkalis | e6d0f4b417 | |
Alfredos-Panagiotis Damkalis | 36a339f6a3 | |
Alfredos-Panagiotis Damkalis | 9b5da399fa | |
Alfredos-Panagiotis Damkalis | 1ec0da8355 | |
Fabian P. Schmidt | 80558c4c28 | |
Fabian P. Schmidt | e681e825e2 | |
Fabian P. Schmidt | f3c395a795 | |
Alfredos-Panagiotis Damkalis | 3f4e5fe03f | |
Alfredos-Panagiotis Damkalis | 6d86d9fb23 | |
Alfredos-Panagiotis Damkalis | d0440e4155 | |
Alfredos-Panagiotis Damkalis | 4b721a4488 | |
Vasilis Tsiligiannis | b63487e8c4 | |
Vasilis Tsiligiannis | f451f255e7 | |
Vasilis Tsiligiannis | d7ebe23830 | |
Vasilis Tsiligiannis | c764b46dff | |
Vasilis Tsiligiannis | 116033384c | |
Vasilis Tsiligiannis | df6c9b0304 | |
Vasilis Tsiligiannis | 632f951531 | |
Fabian P. Schmidt | 97dc4e0652 | |
Vasilis Tsiligiannis | 4a4f96f352 | |
Vasilis Tsiligiannis | c231b9eaa1 | |
Vasilis Tsiligiannis | 5c8c9fb663 | |
Fabian P. Schmidt | 4278c6caa0 | |
deckbsd | 2967e608c3 | |
Fabian P. Schmidt | f72bd9a3f3 | |
deckbsd | 062152aeec | |
Alfredos-Panagiotis Damkalis | 34f2641bef | |
deckbsd | fbd0fbf34e | |
Corey Shields | 72e5d728a2 | |
Corey Shields | e7e400e49a | |
Corey Shields | 8cae425ac9 | |
Vasilis Tsiligiannis | 8710a2df3b | |
Alfredos-Panagiotis Damkalis | 494351e2e7 | |
Vasilis Tsiligiannis | 9f159a2ede | |
Vasilis Tsiligiannis | 95d08f6fa4 | |
Vasilis Tsiligiannis | 6254319426 | |
Vasilis Tsiligiannis | 98b2513ec8 | |
Vasilis Tsiligiannis | 9a5f58864e | |
Vasilis Tsiligiannis | 59c3078872 | |
Vasilis Tsiligiannis | 9fdb19d112 | |
Alfredos-Panagiotis Damkalis | b852ed8372 | |
Alfredos-Panagiotis Damkalis | 281913e9f6 | |
Alfredos-Panagiotis Damkalis | ef40817f61 | |
Patrick Dohmen | 8c340d9e78 | |
Alfredos-Panagiotis Damkalis | e691ddb79e | |
Vasilis Tsiligiannis | 24a353fde0 | |
Vasilis Tsiligiannis | 7ebc04e534 | |
Vasilis Tsiligiannis | 25059fbeac | |
Vasilis Tsiligiannis | 5c4e94ef76 | |
Vasilis Tsiligiannis | 8f3666c2a6 | |
Vasilis Tsiligiannis | 456767786b | |
Vasilis Tsiligiannis | ada835d85d | |
Vasilis Tsiligiannis | bea4b1c9a1 | |
Corey Shields | 3e24773476 | |
Vasilis Tsiligiannis | f5e95c7af7 | |
Vasilis Tsiligiannis | fc90760e7c | |
Vasilis Tsiligiannis | 70ad7ed3de | |
Vasilis Tsiligiannis | 572ac24d44 | |
Vasilis Tsiligiannis | b288a1249d | |
Vasilis Tsiligiannis | 18e377cad5 | |
Corey Shields | 45cf720f99 | |
Corey Shields | cacef2a91e | |
muarachmann | c01cdb1aa7 | |
Corey Shields | 61f9137aae | |
Corey Shields | 6920bbab03 | |
Corey Shields | f80f207c78 | |
Corey Shields | f1420d3e9e | |
Corey Shields | d71114130a | |
Corey Shields | 40460c47bb | |
Corey Shields | fc8f9a8c35 | |
Corey Shields | 9359532df9 | |
Corey Shields | f9336eb0b4 | |
Corey Shields | b0bfe0eea7 | |
Corey Shields | 8271351729 | |
Corey Shields | 91d8ba20bd | |
Corey Shields | b704454486 | |
deckbsd | 965a4f3658 | |
Vasilis Tsiligiannis | 70ffe07f22 | |
Vasilis Tsiligiannis | 5abb3553f9 | |
Vasilis Tsiligiannis | 26662074e5 | |
Vasilis Tsiligiannis | 3404db012c | |
Vasilis Tsiligiannis | ddccf3146e | |
Vasilis Tsiligiannis | f2538355ab | |
Corey Shields | f6e61f158f | |
Corey Shields | 3596ebfb9f | |
Corey Shields | 9989efb453 | |
Corey Shields | 3ce647e374 | |
Corey Shields | 389194b076 | |
Corey Shields | c376e08826 | |
Corey Shields | adfc08bb79 | |
Corey Shields | 6bc64e9217 | |
Corey Shields | fe1f989d7b | |
Corey Shields | 0d4701b100 | |
Corey Shields | f7c4df84f1 | |
Corey Shields | 0da7589c1d | |
Corey Shields | fca0c1d419 | |
Corey Shields | 678de105dd | |
Corey Shields | ec07c59263 | |
Corey Shields | a8a0dc4aa6 | |
Corey Shields | 8b6410ba2c | |
Corey Shields | f90fe277d5 | |
Corey Shields | 2ec2fd2a4d | |
Corey Shields | dcccb1770e | |
Corey Shields | a697548a56 | |
Corey Shields | 8a3ebb3caf | |
Pierros Papadeas | 8a51d27f65 | |
Corey Shields | b7ec119853 | |
Pierros Papadeas | 853381a5dc | |
Pierros Papadeas | d1fb393c79 | |
Pierros Papadeas | 52c91d46a7 | |
Pierros Papadeas | 48ea435093 | |
Fabian P. Schmidt | 203f9a7258 | |
Fabian P. Schmidt | 1dfdbc8cc3 | |
Poonam Mishra | 9a0ef30e03 | |
Corey Shields | 248390a760 | |
Corey Shields | 8734748e50 | |
Corey Shields | dc06369f9a | |
Corey Shields | f734919b61 | |
Corey Shields | f0c0c9bc8b | |
Corey Shields | ff56cb6431 | |
Fabian P. Schmidt | 723fa19ebb | |
Vasilis Tsiligiannis | 3872105dac | |
Corey Shields | d4fb5189d1 | |
Pierros Papadeas | 5433a7a67b | |
Vasilis Tsiligiannis | 1d85a64e6e | |
Vasilis Tsiligiannis | a7e80bb4e5 | |
Vasilis Tsiligiannis | f59efae039 | |
Vasilis Tsiligiannis | 6da2fadf94 | |
Vasilis Tsiligiannis | 4bb87e2e6a | |
Vasilis Tsiligiannis | 084694b02c | |
Vasilis Tsiligiannis | c88a139f89 | |
Vasilis Tsiligiannis | 6ba9a0428a | |
Vasilis Tsiligiannis | 811b3a8a8f | |
Vasilis Tsiligiannis | 826f7bbe9f | |
Vasilis Tsiligiannis | be4e2fd1fc | |
Vasilis Tsiligiannis | 45abdcd33a | |
Vasilis Tsiligiannis | cb2c4899a9 |
|
@ -6,6 +6,7 @@ env
|
|||
.env
|
||||
.cache
|
||||
*.egg-info
|
||||
*.DS_Store
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
@ -16,6 +17,10 @@ pip-log.txt
|
|||
.tox
|
||||
nosetests.xml
|
||||
|
||||
# build artifacts
|
||||
build
|
||||
dist
|
||||
|
||||
# Sqlite
|
||||
*.db
|
||||
*.sqlite
|
||||
|
@ -23,6 +28,7 @@ nosetests.xml
|
|||
|
||||
# Media & Static
|
||||
media
|
||||
/staticfiles/*
|
||||
/db/static/lib/
|
||||
node_modules
|
||||
|
||||
|
@ -31,3 +37,9 @@ celerybeat-schedule
|
|||
|
||||
# Documentation
|
||||
/docs/_build/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
|
264
.gitlab-ci.yml
264
.gitlab-ci.yml
|
@ -1,12 +1,74 @@
|
|||
variables:
|
||||
GITLAB_CI_IMAGE_ALPINE: 'alpine:3.9'
|
||||
GITLAB_CI_IMAGE_DOCKER: 'docker:20.10.6'
|
||||
GITLAB_CI_IMAGE_NODE: 'node:13.12'
|
||||
GITLAB_CI_IMAGE_PYTHON: 'python:3.9.12'
|
||||
GITLAB_CI_IMAGE_OPENAPI_GENERATOR_CLI: 'openapitools/openapi-generator-cli:v5.3.0'
|
||||
GITLAB_CI_IMAGE_SENTRY_CLI: 'getsentry/sentry-cli'
|
||||
GITLAB_CI_PYPI_DOCKER_COMPOSE: 'docker-compose~=1.23.0'
|
||||
GITLAB_CI_PYPI_TOX: 'tox~=3.20.0'
|
||||
stages:
|
||||
- schema
|
||||
- api
|
||||
- static
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
- sentry_release
|
||||
- trigger
|
||||
- security
|
||||
|
||||
# 'schema' stage
|
||||
schema:
|
||||
stage: schema
|
||||
needs: []
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
script:
|
||||
- pip install --no-cache-dir --no-deps -r "requirements.txt" --force-reinstall .
|
||||
- >-
|
||||
./manage.py spectacular
|
||||
--file satnogs-db-api-client/api-schema.yml
|
||||
--validate
|
||||
--fail-on-warn
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
paths:
|
||||
- satnogs-db-api-client
|
||||
|
||||
# 'api' stage
|
||||
api:
|
||||
stage: api
|
||||
needs:
|
||||
- job: schema
|
||||
artifacts: true
|
||||
image: ${GITLAB_CI_IMAGE_OPENAPI_GENERATOR_CLI}
|
||||
script:
|
||||
- >-
|
||||
docker-entrypoint.sh
|
||||
generate
|
||||
-i satnogs-db-api-client/api-schema.yml
|
||||
-g python
|
||||
-o satnogs-db-api-client
|
||||
-c satnogs-db-api-client/openapi-generator-config.json
|
||||
- >-
|
||||
docker-entrypoint.sh
|
||||
generate
|
||||
-i satnogs-db-api-client/api-schema.yml
|
||||
-g html2
|
||||
-o satnogs-db-api-client/html2
|
||||
-c satnogs-db-api-client/openapi-generator-config.json
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
paths:
|
||||
- satnogs-db-api-client
|
||||
|
||||
# 'static' stage
|
||||
static_js_css:
|
||||
stage: static
|
||||
image: node:11.13
|
||||
needs: []
|
||||
image: ${GITLAB_CI_IMAGE_NODE}
|
||||
script:
|
||||
- npm ci
|
||||
- node_modules/.bin/gulp
|
||||
|
@ -15,28 +77,25 @@ static_js_css:
|
|||
when: always
|
||||
paths:
|
||||
- db/static/lib
|
||||
static_python:
|
||||
static:
|
||||
stage: static
|
||||
image: python:2
|
||||
needs: []
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install tox~=3.8.0
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- tox -e "py2-{flake8,isort,yapf}"
|
||||
static_python3:
|
||||
stage: static
|
||||
image: python:3
|
||||
before_script:
|
||||
- pip install tox~=3.8.0
|
||||
script:
|
||||
- tox -e "py3-{flake8,isort,yapf}"
|
||||
- tox -e "flake8,isort,yapf,pylint"
|
||||
|
||||
# 'build' stage
|
||||
docs:
|
||||
stage: build
|
||||
image: python:2
|
||||
needs: []
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install sphinx_rtd_theme
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- cd docs
|
||||
- make html SPHINXOPTS="-W"
|
||||
- rm -rf docs/_build
|
||||
- tox -e "docs"
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
@ -44,45 +103,63 @@ docs:
|
|||
- docs/_build/html
|
||||
build:
|
||||
stage: build
|
||||
image: python:2
|
||||
needs:
|
||||
- job: static_js_css
|
||||
artifacts: true
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- rm -rf dist
|
||||
- python setup.py sdist bdist_wheel
|
||||
- tox -e build
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
paths:
|
||||
- dist
|
||||
build3:
|
||||
build_api:
|
||||
stage: build
|
||||
image: python:3
|
||||
needs:
|
||||
- job: api
|
||||
artifacts: true
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- cd satnogs-db-api-client
|
||||
- rm -rf dist
|
||||
- python setup.py sdist bdist_wheel
|
||||
- tox -e build
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
paths:
|
||||
- dist
|
||||
- satnogs-db-api-client/dist
|
||||
|
||||
# 'test' stage
|
||||
test:
|
||||
stage: test
|
||||
image: python:2
|
||||
needs:
|
||||
- job: static_js_css
|
||||
artifacts: true
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install tox~=3.8.0
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- tox -e deps,pytest
|
||||
|
||||
# 'deploy' stage
|
||||
docker:
|
||||
stage: deploy
|
||||
image: docker:18.09
|
||||
image: ${GITLAB_CI_IMAGE_DOCKER}
|
||||
services:
|
||||
- docker:18.09-dind
|
||||
- ${GITLAB_CI_IMAGE_DOCKER}-dind
|
||||
before_script:
|
||||
- apk --update add py-pip
|
||||
- pip install docker-compose~=1.23.0
|
||||
- pip install "$GITLAB_CI_PYPI_DOCKER_COMPOSE"
|
||||
script:
|
||||
- |
|
||||
[ -z "$CI_REGISTRY_IMAGE" ] || {
|
||||
CACHE_IMAGE="$CI_REGISTRY_IMAGE/satnogs-db:$CI_COMMIT_REF_NAME}"
|
||||
CACHE_IMAGE="$CI_REGISTRY_IMAGE/satnogs-db:$CI_COMMIT_REF_NAME"
|
||||
[ -z "$CI_COMMIT_TAG" ] || CACHE_IMAGE="$CI_REGISTRY_IMAGE/satnogs-db:latest"
|
||||
export CACHE_IMAGE
|
||||
}
|
||||
|
@ -111,12 +188,76 @@ docker:
|
|||
refs:
|
||||
- master
|
||||
- tags
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- rm -rf dist
|
||||
- tox -e "upload"
|
||||
only:
|
||||
refs:
|
||||
- tags
|
||||
variables:
|
||||
- $PYPI_USERNAME
|
||||
- $PYPI_PASSWORD
|
||||
except:
|
||||
- triggers
|
||||
deploy_api:
|
||||
stage: deploy
|
||||
image: ${GITLAB_CI_IMAGE_PYTHON}
|
||||
before_script:
|
||||
- pip install "$GITLAB_CI_PYPI_TOX"
|
||||
script:
|
||||
- cd satnogs-db-api-client
|
||||
- rm -rf dist
|
||||
- tox -e "upload"
|
||||
only:
|
||||
refs:
|
||||
- tags
|
||||
variables:
|
||||
- $PYPI_USERNAME
|
||||
- $PYPI_PASSWORD
|
||||
except:
|
||||
- triggers
|
||||
pages:
|
||||
stage: deploy
|
||||
image: ${GITLAB_CI_IMAGE_ALPINE}
|
||||
script:
|
||||
- mv docs/_build/html/ public/
|
||||
- mv satnogs-db-api-client/html2/ public/api/
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- tags
|
||||
|
||||
# 'sentry_release' stage
|
||||
sentry_release:
|
||||
stage: sentry_release
|
||||
image: ${GITLAB_CI_IMAGE_SENTRY_CLI}
|
||||
script:
|
||||
- sentry-cli releases new --finalize -p ${CI_PROJECT_NAME} ${CI_PROJECT_NAME}@${CI_COMMIT_TAG}
|
||||
- sentry-cli releases set-commits --auto ${CI_PROJECT_NAME}@${CI_COMMIT_TAG}
|
||||
only:
|
||||
refs:
|
||||
- tags
|
||||
variables:
|
||||
- $SENTRY_AUTH_TOKEN
|
||||
- $SENTRY_ORG
|
||||
|
||||
# 'trigger' stage
|
||||
trigger_master:
|
||||
stage: trigger
|
||||
image: alpine:3.9
|
||||
needs:
|
||||
- job: docker
|
||||
artifacts: false
|
||||
image: ${GITLAB_CI_IMAGE_ALPINE}
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
script:
|
||||
- PIPELINE_TRIGGERS_MASTER=$(echo "$PIPELINE_TRIGGERS_MASTER" | sed 's/{{CI_COMMIT_SHORT_SHA}}/'"$CI_COMMIT_SHORT_SHA"'/g')
|
||||
- for trigger in $PIPELINE_TRIGGERS_MASTER; do curl -X POST "$trigger"; done
|
||||
only:
|
||||
refs:
|
||||
|
@ -125,23 +266,66 @@ trigger_master:
|
|||
- $PIPELINE_TRIGGERS_MASTER
|
||||
trigger_latest:
|
||||
stage: trigger
|
||||
image: alpine:3.9
|
||||
needs:
|
||||
- job: docker
|
||||
artifacts: false
|
||||
image: ${GITLAB_CI_IMAGE_ALPINE}
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
script:
|
||||
- PIPELINE_TRIGGERS_LATEST=$(echo "$PIPELINE_TRIGGERS_LATEST" | sed 's/{{CI_COMMIT_TAG}}/'"$CI_COMMIT_TAG"'/g')
|
||||
- for trigger in $PIPELINE_TRIGGERS_LATEST; do curl -X POST "$trigger"; done
|
||||
only:
|
||||
refs:
|
||||
- tags
|
||||
variables:
|
||||
- $PIPELINE_TRIGGERS_LATEST
|
||||
pages:
|
||||
stage: deploy
|
||||
image: alpine
|
||||
script:
|
||||
- mv docs/_build/html/ public/
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
only:
|
||||
- tags
|
||||
|
||||
# 'security' stage
|
||||
include:
|
||||
- template: Security/Container-Scanning.gitlab-ci.yml
|
||||
- template: Security/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
- template: Security/License-Scanning.gitlab-ci.yml
|
||||
container_scanning:
|
||||
stage: security
|
||||
needs:
|
||||
- job: docker
|
||||
artifacts: false
|
||||
variables:
|
||||
CI_APPLICATION_REPOSITORY: ${CI_REGISTRY_IMAGE}/satnogs-db
|
||||
CI_APPLICATION_TAG: ${CI_COMMIT_REF_NAME}
|
||||
rules:
|
||||
- if: $CI_REGISTRY_IMAGE && $CI_COMMIT_BRANCH == "master"
|
||||
- if: $CI_REGISTRY_IMAGE && $CI_COMMIT_TAG
|
||||
dependency_scanning:
|
||||
stage: security
|
||||
needs:
|
||||
- job: api
|
||||
artifacts: true
|
||||
variables:
|
||||
DS_DEFAULT_ANALYZERS: 'gemnasium,gemnasium-python,retire.js'
|
||||
gemnasium-python-dependency_scanning:
|
||||
before_script:
|
||||
- apt-get -q update
|
||||
- apt-get -qy install libmariadb-dev python3-pil libjpeg-dev
|
||||
sast:
|
||||
stage: security
|
||||
needs:
|
||||
- job: api
|
||||
artifacts: true
|
||||
variables:
|
||||
SAST_DISABLE_BABEL: 'true'
|
||||
secret_detection:
|
||||
stage: security
|
||||
needs:
|
||||
- job: api
|
||||
artifacts: true
|
||||
license_scanning:
|
||||
stage: security
|
||||
needs:
|
||||
- job: api
|
||||
artifacts: true
|
||||
- job: static_js_css
|
||||
artifacts: true
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
[MASTER]
|
||||
ignore=_version.py,migrations
|
||||
load-plugins=pylint_django
|
||||
ignored-argument-names=args|kwargs
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=
|
||||
C0412,
|
||||
R0801, # needs to remain disabled see https://github.com/PyCQA/pylint/issues/214
|
||||
|
||||
[TYPECHECK]
|
||||
# zmq.{EAGAIN,RCVTIMEO,XPUB} is dynamically generated and so pylint
|
||||
# doesn't see it, causing false positives.
|
||||
generated-members=
|
||||
zmq.EAGAIN,
|
||||
zmq.RCVTIMEO,
|
||||
zmq.XPUB
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
version: '2'
|
||||
formats:
|
||||
- 'epub'
|
||||
- 'pdf'
|
||||
python:
|
||||
version: '3.8'
|
||||
install:
|
||||
- path: '.'
|
||||
- requirements: 'docs/requirements.txt'
|
||||
build:
|
||||
image: 'latest'
|
|
@ -1,2 +1,8 @@
|
|||
node_modules
|
||||
db/_version.py
|
||||
db/base/migrations
|
||||
db/*/migrations
|
||||
satnogs-db-api-client
|
||||
.tox
|
||||
build
|
||||
docs
|
||||
versioneer.py
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
FROM python:2
|
||||
MAINTAINER SatNOGS project <dev@satnogs.org>
|
||||
FROM python:3.9.12
|
||||
LABEL maintainer="SatNOGS project <dev@satnogs.org>"
|
||||
|
||||
WORKDIR /workdir/
|
||||
|
||||
|
@ -8,6 +8,7 @@ RUN groupadd -r satnogs \
|
|||
&& install -d -m 755 -o satnogs -g satnogs /var/run/celery
|
||||
|
||||
COPY requirements.txt /usr/local/src/satnogs-db/
|
||||
ARG SATNOGS_DECODERS_VERSION
|
||||
RUN pip install \
|
||||
--no-cache-dir \
|
||||
--no-deps \
|
||||
|
|
7
LICENSE
7
LICENSE
|
@ -1,7 +1,7 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
|
@ -658,5 +658,4 @@ specific requirements.
|
|||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# SatNOGS DB
|
||||
|
||||
SatNOGS DB is a transmitter suggestions and crowd-sourcing app.
|
||||
|
||||
## Contribute
|
||||
|
||||
Check out the [documentation](https://docs.satnogs.org/projects/satnogs-db/en/stable/) on how to setup a local development instance.
|
||||
|
||||
The main repository lives on [Gitlab](https://gitlab.com/librespacefoundation/satnogs/satnogs-db).
|
||||
|
||||
## Join
|
||||
|
||||
[![matrix](https://img.shields.io/badge/Matrix-%23satnogs:matrix.org-blue.svg)](https://riot.im/app/#/room/#satnogs:matrix.org)
|
||||
[![irc](https://img.shields.io/badge/IRC-%23satnogs%20on%20freenode-blue.svg)](https://webchat.freenode.net/?channels=satnogs)
|
||||
[![forum](https://img.shields.io/badge/forum-discourse-blue.svg)](https://community.libre.space/c/satnogs)
|
||||
|
||||
## Current Development
|
||||
|
||||
[![kanban](https://img.shields.io/badge/kanban-board-lightgray.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/boards/345706)
|
||||
[![build](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/badges/master/build.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/commits/master)
|
||||
[![coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/badges/master/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/commits/master)
|
||||
|
||||
## License
|
||||
|
||||
[![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
|
||||
[![Libre Space Foundation](https://img.shields.io/badge/%C2%A9%202014--2019-Libre%20Space%20Foundation-6672D8.svg)](https://librespacefoundation.org/)
|
115
README.md
115
README.md
|
@ -1,26 +1,109 @@
|
|||
# SatNOGS DB
|
||||
# SatNOGS DB Fork
|
||||
This lesser fork is for exploring the SatNOGS DB.
|
||||
|
||||
SatNOGS DB is a transmitter suggestions and crowd-sourcing app.
|
||||
* https://spacecruft.org/spacecruft/satnogs-db
|
||||
|
||||
## Contribute
|
||||
|
||||
Check out the [documentation](https://docs.satnogs.org/en/latest/satnogs-db/docs/) on how to setup a local development instance.
|
||||
# Install Dependencies
|
||||
Using Debian Stable (Bullseye/11).
|
||||
|
||||
The main repository lives on [Gitlab](https://gitlab.com/librespacefoundation/satnogs/satnogs-db).
|
||||
```
|
||||
sudo apt install git libmariadb-dev mariadb-server npm python3-pip sqlite3 virtualenvwrapper
|
||||
```
|
||||
|
||||
## Join
|
||||
# Repo setup
|
||||
I setup my `git` repo thusly.
|
||||
|
||||
[![matrix](https://img.shields.io/badge/Matrix-%23satnogs:matrix.org-blue.svg)](https://riot.im/app/#/room/#satnogs:matrix.org)
|
||||
[![irc](https://img.shields.io/badge/IRC-%23satnogs%20on%20freenode-blue.svg)](https://webchat.freenode.net/?channels=satnogs)
|
||||
[![forum](https://img.shields.io/badge/forum-discourse-blue.svg)](https://community.libre.space/c/satnogs)
|
||||
```
|
||||
git clone git@spacecruft.org:spacecruft/satnogs-db.git
|
||||
cd satnogs-db
|
||||
git remote add upstream https://gitlab.com/librespacefoundation/satnogs/satnogs-db.git
|
||||
git fetch upstream
|
||||
git checkout remotes/upstream/master
|
||||
git branch spacecruft
|
||||
git checkout spacecruft
|
||||
git push --set-upstream origin spacecruft
|
||||
# set to default branch in gitea
|
||||
```
|
||||
|
||||
## Current Development
|
||||
|
||||
[![kanban](https://img.shields.io/badge/kanban-board-lightgray.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/boards/345706)
|
||||
[![build](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/badges/dev/build.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/commits/dev)
|
||||
[![coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/badges/dev/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-db/commits/dev)
|
||||
# Setup
|
||||
Set up the environment.
|
||||
|
||||
## License
|
||||
To use `virtualenvwrapper`, you need to add it to the PATH.
|
||||
The easiest way is to just add this like to the end of
|
||||
`~/.bashrc`.
|
||||
|
||||
```
|
||||
source /usr/share/virtualenvwrapper/virtualenvwrapper.sh
|
||||
```
|
||||
|
||||
Then log out and back in or just re-source the file.
|
||||
This will enable the `mkvirtualenv` and `workon` commands.
|
||||
|
||||
|
||||
```
|
||||
cd satnogs-db
|
||||
mkvirtualenv satnogs-db -a .
|
||||
pip install --upgrade pip
|
||||
cp env-dist .env
|
||||
```
|
||||
|
||||
Then edit the `.env` file to add `ALLOWED_HOSTS` for remote acccess
|
||||
on LAN. Use IP address of *server*.
|
||||
|
||||
```
|
||||
ALLOWED_HOSTS='192.168.1.100'
|
||||
```
|
||||
|
||||
|
||||
# Front End Dependencies
|
||||
Install front end.
|
||||
Each command takes one minute or so to run.
|
||||
|
||||
```
|
||||
npm install
|
||||
./node_modules/.bin/gulp
|
||||
```
|
||||
|
||||
|
||||
# Populate Database
|
||||
Run thusly:
|
||||
|
||||
```
|
||||
workon satnogs-db # if you aren't already in the environment
|
||||
./bin/djangoctl.sh develop .
|
||||
```
|
||||
|
||||
Set up database in another terminal, when above command is ready
|
||||
and listening on port 8000:
|
||||
|
||||
```
|
||||
cd satnogs-db
|
||||
workon satnogs-db
|
||||
./bin/djangoctl.sh initialize
|
||||
```
|
||||
|
||||
# Access
|
||||
May need to open firewall on server, `TCP/8000`.
|
||||
|
||||
|
||||
# Use
|
||||
In web browser go to server IP, port 8000.
|
||||
|
||||
http://192.168.1.1:8000/
|
||||
|
||||
Log in with super user created above when initializing.
|
||||
It will verify email address. Look in the output of the
|
||||
terminal running the django command above for the URL
|
||||
to validate the email address (assuming no email is
|
||||
actually used).
|
||||
|
||||
|
||||
# Upstream
|
||||
See upstream `README-upstream.md`.
|
||||
|
||||
* https://db.satnogs.org/
|
||||
* https://gitlab.com/librespacefoundation/satnogs/satnogs-db.git
|
||||
* https://docs.satnogs.org/projects/satnogs-db/en/stable/
|
||||
|
||||
[![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
|
||||
[![Libre Space Foundation](https://img.shields.io/badge/%C2%A9%202014--2017-Libre%20Space%20Foundation-6672D8.svg)](https://librespacefoundation.org/)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,8 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Auth0LoginConfig(AppConfig):
|
||||
name = 'auth0login'
|
|
@ -1,36 +0,0 @@
|
|||
import requests
|
||||
|
||||
from social_core.backends.oauth import BaseOAuth2
|
||||
|
||||
|
||||
class Auth0(BaseOAuth2):
|
||||
"""Auth0 OAuth authentication backend"""
|
||||
name = 'auth0'
|
||||
SCOPE_SEPARATOR = ' '
|
||||
ACCESS_TOKEN_METHOD = 'POST'
|
||||
EXTRA_DATA = [('email', 'email')]
|
||||
|
||||
def authorization_url(self):
|
||||
"""Return the authorization endpoint."""
|
||||
return "https://" + self.setting('DOMAIN') + "/authorize"
|
||||
|
||||
def access_token_url(self):
|
||||
"""Return the token endpoint."""
|
||||
return "https://" + self.setting('DOMAIN') + "/oauth/token"
|
||||
|
||||
def get_user_id(self, details, response):
|
||||
"""Return current user id."""
|
||||
return details['user_id']
|
||||
|
||||
def get_user_details(self, response):
|
||||
url = 'https://' + self.setting('DOMAIN') + '/userinfo'
|
||||
headers = {'authorization': 'Bearer ' + response['access_token']}
|
||||
resp = requests.get(url, headers=headers)
|
||||
userinfo = resp.json()
|
||||
|
||||
return {
|
||||
'username': userinfo['nickname'],
|
||||
'email': userinfo['email'],
|
||||
# 'first_name': userinfo['name'],
|
||||
'user_id': userinfo['sub']
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# from django.db import models
|
||||
|
||||
# Create your models here.
|
|
@ -1,6 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,9 +0,0 @@
|
|||
from django.conf.urls import include, url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url('^$', views.index),
|
||||
url(r'^', include('django.contrib.auth.urls', namespace='auth')),
|
||||
url(r'^', include('social_django.urls', namespace='social')),
|
||||
]
|
|
@ -1,7 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'index.html')
|
|
@ -72,7 +72,7 @@ run() {
|
|||
run_celery() {
|
||||
case "$1" in
|
||||
worker|beat)
|
||||
exec celery -A "$DJANGO_APP" "$1" -l INFO --workdir "$CELERY_VAR_RUN"
|
||||
exec celery --workdir "$CELERY_VAR_RUN" -A "$DJANGO_APP" "$1" -l INFO
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
"""pytest configuration file"""
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def celery_config():
|
||||
return {
|
||||
'broker_url': 'memory://',
|
||||
'result_backend': 'rpc',
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
# Script to refresh requirements.txt file
|
||||
#
|
||||
# Copyright (C) 2019 Libre Space Foundation <https://libre.space/>
|
||||
# Copyright (C) 2019-2022 Libre Space Foundation <https://libre.space/>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,13 +17,36 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
EXCLUDE_REGEXP="^\\(pkg-resources\\|satnogs-db\\)"
|
||||
COMPATIBLE_REGEXP="^\\(satnogsdecoders\\)"
|
||||
EXCLUDE_REGEXP="^\\(pkg[-_]resources\\|satnogs-db\\)"
|
||||
COMPATIBLE_REGEXP="^\\(satnogs-decoders\\)"
|
||||
VIRTUALENV_DIR=$(mktemp -d)
|
||||
PIP_COMMAND="$VIRTUALENV_DIR/bin/pip"
|
||||
PYTHON_VERSION="3.9"
|
||||
REQUIREMENTS="
|
||||
comm
|
||||
grep
|
||||
sed
|
||||
sort
|
||||
virtualenv
|
||||
"
|
||||
|
||||
# Check for required utilities
|
||||
for req in $REQUIREMENTS; do
|
||||
if ! which "$req" >/dev/null; then
|
||||
if [ -z "$has_missing" ]; then
|
||||
echo "$(basename "$0"): Missing script requirements!" 1>&2
|
||||
echo "Please install:" 1>&2
|
||||
has_missing=1
|
||||
fi
|
||||
echo " - '$req'" 1>&2
|
||||
fi
|
||||
done
|
||||
if [ -n "$has_missing" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtualenv
|
||||
virtualenv "$VIRTUALENV_DIR"
|
||||
virtualenv -p python$PYTHON_VERSION "$VIRTUALENV_DIR"
|
||||
|
||||
# Install package with dependencies
|
||||
"$PIP_COMMAND" install --no-cache-dir --force-reinstall .
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
"""The core django app for SatNOGS DB"""
|
||||
from ._version import get_versions
|
||||
from .celery import app as celery_app # noqa
|
||||
from .celery import APP as celery_app # noqa
|
||||
|
||||
__all__ = ['celery_app']
|
||||
|
||||
|
|
266
db/_version.py
266
db/_version.py
|
@ -6,7 +6,7 @@
|
|||
# that just contains the computed version number.
|
||||
|
||||
# This file is released into the public domain. Generated by
|
||||
# versioneer-0.18 (https://github.com/warner/python-versioneer)
|
||||
# versioneer-0.22 (https://github.com/python-versioneer/python-versioneer)
|
||||
|
||||
"""Git implementation of _version.py."""
|
||||
|
||||
|
@ -15,6 +15,8 @@ import os
|
|||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Callable, Dict
|
||||
import functools
|
||||
|
||||
|
||||
def get_keywords():
|
||||
|
@ -52,12 +54,12 @@ class NotThisMethod(Exception):
|
|||
"""Exception raised if a method is not valid for the current scenario."""
|
||||
|
||||
|
||||
LONG_VERSION_PY = {}
|
||||
HANDLERS = {}
|
||||
LONG_VERSION_PY: Dict[str, str] = {}
|
||||
HANDLERS: Dict[str, Dict[str, Callable]] = {}
|
||||
|
||||
|
||||
def register_vcs_handler(vcs, method): # decorator
|
||||
"""Decorator to mark a method as the handler for a particular VCS."""
|
||||
"""Create decorator to mark a method as the handler of a VCS."""
|
||||
def decorate(f):
|
||||
"""Store f in HANDLERS[vcs][method]."""
|
||||
if vcs not in HANDLERS:
|
||||
|
@ -71,17 +73,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
|
|||
env=None):
|
||||
"""Call the given command(s)."""
|
||||
assert isinstance(commands, list)
|
||||
p = None
|
||||
for c in commands:
|
||||
process = None
|
||||
|
||||
popen_kwargs = {}
|
||||
if sys.platform == "win32":
|
||||
# This hides the console window if pythonw.exe is used
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
popen_kwargs["startupinfo"] = startupinfo
|
||||
|
||||
for command in commands:
|
||||
try:
|
||||
dispcmd = str([c] + args)
|
||||
dispcmd = str([command] + args)
|
||||
# remember shell=False, so use git.cmd on windows, not just git
|
||||
p = subprocess.Popen([c] + args, cwd=cwd, env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=(subprocess.PIPE if hide_stderr
|
||||
else None))
|
||||
process = subprocess.Popen([command] + args, cwd=cwd, env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=(subprocess.PIPE if hide_stderr
|
||||
else None), **popen_kwargs)
|
||||
break
|
||||
except EnvironmentError:
|
||||
except OSError:
|
||||
e = sys.exc_info()[1]
|
||||
if e.errno == errno.ENOENT:
|
||||
continue
|
||||
|
@ -93,15 +103,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
|
|||
if verbose:
|
||||
print("unable to find command, tried %s" % (commands,))
|
||||
return None, None
|
||||
stdout = p.communicate()[0].strip()
|
||||
if sys.version_info[0] >= 3:
|
||||
stdout = stdout.decode()
|
||||
if p.returncode != 0:
|
||||
stdout = process.communicate()[0].strip().decode()
|
||||
if process.returncode != 0:
|
||||
if verbose:
|
||||
print("unable to run %s (error)" % dispcmd)
|
||||
print("stdout was %s" % stdout)
|
||||
return None, p.returncode
|
||||
return stdout, p.returncode
|
||||
return None, process.returncode
|
||||
return stdout, process.returncode
|
||||
|
||||
|
||||
def versions_from_parentdir(parentdir_prefix, root, verbose):
|
||||
|
@ -113,15 +121,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
|
|||
"""
|
||||
rootdirs = []
|
||||
|
||||
for i in range(3):
|
||||
for _ in range(3):
|
||||
dirname = os.path.basename(root)
|
||||
if dirname.startswith(parentdir_prefix):
|
||||
return {"version": dirname[len(parentdir_prefix):],
|
||||
"full-revisionid": None,
|
||||
"dirty": False, "error": None, "date": None}
|
||||
else:
|
||||
rootdirs.append(root)
|
||||
root = os.path.dirname(root) # up a level
|
||||
rootdirs.append(root)
|
||||
root = os.path.dirname(root) # up a level
|
||||
|
||||
if verbose:
|
||||
print("Tried directories %s but none started with prefix %s" %
|
||||
|
@ -138,22 +145,21 @@ def git_get_keywords(versionfile_abs):
|
|||
# _version.py.
|
||||
keywords = {}
|
||||
try:
|
||||
f = open(versionfile_abs, "r")
|
||||
for line in f.readlines():
|
||||
if line.strip().startswith("git_refnames ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["refnames"] = mo.group(1)
|
||||
if line.strip().startswith("git_full ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["full"] = mo.group(1)
|
||||
if line.strip().startswith("git_date ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["date"] = mo.group(1)
|
||||
f.close()
|
||||
except EnvironmentError:
|
||||
with open(versionfile_abs, "r") as fobj:
|
||||
for line in fobj:
|
||||
if line.strip().startswith("git_refnames ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["refnames"] = mo.group(1)
|
||||
if line.strip().startswith("git_full ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["full"] = mo.group(1)
|
||||
if line.strip().startswith("git_date ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["date"] = mo.group(1)
|
||||
except OSError:
|
||||
pass
|
||||
return keywords
|
||||
|
||||
|
@ -161,10 +167,14 @@ def git_get_keywords(versionfile_abs):
|
|||
@register_vcs_handler("git", "keywords")
|
||||
def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
||||
"""Get version information from git keywords."""
|
||||
if not keywords:
|
||||
raise NotThisMethod("no keywords at all, weird")
|
||||
if "refnames" not in keywords:
|
||||
raise NotThisMethod("Short version file found")
|
||||
date = keywords.get("date")
|
||||
if date is not None:
|
||||
# Use only the last line. Previous lines may contain GPG signature
|
||||
# information.
|
||||
date = date.splitlines()[-1]
|
||||
|
||||
# git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
|
||||
# datestamp. However we prefer "%ci" (which expands to an "ISO-8601
|
||||
# -like" string, which we must then edit to make compliant), because
|
||||
|
@ -177,11 +187,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
|||
if verbose:
|
||||
print("keywords are unexpanded, not using")
|
||||
raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
|
||||
refs = set([r.strip() for r in refnames.strip("()").split(",")])
|
||||
refs = {r.strip() for r in refnames.strip("()").split(",")}
|
||||
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
|
||||
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
|
||||
TAG = "tag: "
|
||||
tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
|
||||
tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
|
||||
if not tags:
|
||||
# Either we're using git < 1.8.3, or there really are no tags. We use
|
||||
# a heuristic: assume all version tags have a digit. The old git %d
|
||||
|
@ -190,7 +200,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
|||
# between branches and tags. By ignoring refnames without digits, we
|
||||
# filter out many common branch names like "release" and
|
||||
# "stabilization", as well as "HEAD" and "master".
|
||||
tags = set([r for r in refs if re.search(r'\d', r)])
|
||||
tags = {r for r in refs if re.search(r'\d', r)}
|
||||
if verbose:
|
||||
print("discarding '%s', no digits" % ",".join(refs - tags))
|
||||
if verbose:
|
||||
|
@ -199,6 +209,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
|||
# sorting will prefer e.g. "2.0" over "2.0rc1"
|
||||
if ref.startswith(tag_prefix):
|
||||
r = ref[len(tag_prefix):]
|
||||
# Filter out refs that exactly match prefix or that don't start
|
||||
# with a number once the prefix is stripped (mostly a concern
|
||||
# when prefix is '')
|
||||
if not re.match(r'\d', r):
|
||||
continue
|
||||
if verbose:
|
||||
print("picking %s" % r)
|
||||
return {"version": r,
|
||||
|
@ -214,7 +229,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
|||
|
||||
|
||||
@register_vcs_handler("git", "pieces_from_vcs")
|
||||
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
||||
def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command):
|
||||
"""Get version from 'git describe' in the root of the source tree.
|
||||
|
||||
This only gets called if the git-archive 'subst' keywords were *not*
|
||||
|
@ -225,24 +240,32 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
|||
if sys.platform == "win32":
|
||||
GITS = ["git.cmd", "git.exe"]
|
||||
|
||||
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
|
||||
hide_stderr=True)
|
||||
# GIT_DIR can interfere with correct operation of Versioneer.
|
||||
# It may be intended to be passed to the Versioneer-versioned project,
|
||||
# but that should not change where we get our version from.
|
||||
env = os.environ.copy()
|
||||
env.pop("GIT_DIR", None)
|
||||
runner = functools.partial(runner, env=env)
|
||||
|
||||
_, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
|
||||
hide_stderr=True)
|
||||
if rc != 0:
|
||||
if verbose:
|
||||
print("Directory %s not under git control" % root)
|
||||
raise NotThisMethod("'git rev-parse --git-dir' returned error")
|
||||
|
||||
MATCH_ARGS = ["--match", "%s*" % tag_prefix] if tag_prefix else []
|
||||
|
||||
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
|
||||
# if there isn't one, this yields HEX[-dirty] (no NUM)
|
||||
describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
|
||||
"--always", "--long",
|
||||
"--match", "%s*" % tag_prefix],
|
||||
cwd=root)
|
||||
describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty",
|
||||
"--always", "--long", *MATCH_ARGS],
|
||||
cwd=root)
|
||||
# --long was added in git-1.5.5
|
||||
if describe_out is None:
|
||||
raise NotThisMethod("'git describe' failed")
|
||||
describe_out = describe_out.strip()
|
||||
full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
|
||||
full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
|
||||
if full_out is None:
|
||||
raise NotThisMethod("'git rev-parse' failed")
|
||||
full_out = full_out.strip()
|
||||
|
@ -252,6 +275,39 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
|||
pieces["short"] = full_out[:7] # maybe improved later
|
||||
pieces["error"] = None
|
||||
|
||||
branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
|
||||
cwd=root)
|
||||
# --abbrev-ref was added in git-1.6.3
|
||||
if rc != 0 or branch_name is None:
|
||||
raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
|
||||
branch_name = branch_name.strip()
|
||||
|
||||
if branch_name == "HEAD":
|
||||
# If we aren't exactly on a branch, pick a branch which represents
|
||||
# the current commit. If all else fails, we are on a branchless
|
||||
# commit.
|
||||
branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
|
||||
# --contains was added in git-1.5.4
|
||||
if rc != 0 or branches is None:
|
||||
raise NotThisMethod("'git branch --contains' returned error")
|
||||
branches = branches.split("\n")
|
||||
|
||||
# Remove the first line if we're running detached
|
||||
if "(" in branches[0]:
|
||||
branches.pop(0)
|
||||
|
||||
# Strip off the leading "* " from the list of branches.
|
||||
branches = [branch[2:] for branch in branches]
|
||||
if "master" in branches:
|
||||
branch_name = "master"
|
||||
elif not branches:
|
||||
branch_name = None
|
||||
else:
|
||||
# Pick the first branch that is returned. Good or bad.
|
||||
branch_name = branches[0]
|
||||
|
||||
pieces["branch"] = branch_name
|
||||
|
||||
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
|
||||
# TAG might have hyphens.
|
||||
git_describe = describe_out
|
||||
|
@ -268,7 +324,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
|||
# TAG-NUM-gHEX
|
||||
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
|
||||
if not mo:
|
||||
# unparseable. Maybe git-describe is misbehaving?
|
||||
# unparsable. Maybe git-describe is misbehaving?
|
||||
pieces["error"] = ("unable to parse git-describe output: '%s'"
|
||||
% describe_out)
|
||||
return pieces
|
||||
|
@ -293,13 +349,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
|||
else:
|
||||
# HEX: no tags
|
||||
pieces["closest-tag"] = None
|
||||
count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
|
||||
cwd=root)
|
||||
count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
|
||||
pieces["distance"] = int(count_out) # total number of commits
|
||||
|
||||
# commit date: see ISO-8601 comment in git_versions_from_keywords()
|
||||
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
|
||||
cwd=root)[0].strip()
|
||||
date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
|
||||
# Use only the last line. Previous lines may contain GPG signature
|
||||
# information.
|
||||
date = date.splitlines()[-1]
|
||||
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||
|
||||
return pieces
|
||||
|
@ -337,19 +394,67 @@ def render_pep440(pieces):
|
|||
return rendered
|
||||
|
||||
|
||||
def render_pep440_pre(pieces):
|
||||
"""TAG[.post.devDISTANCE] -- No -dirty.
|
||||
def render_pep440_branch(pieces):
|
||||
"""TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
|
||||
|
||||
The ".dev0" means not master branch. Note that .dev0 sorts backwards
|
||||
(a feature branch will appear "older" than the master branch).
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.post.devDISTANCE
|
||||
1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"]:
|
||||
rendered += ".post.dev%d" % pieces["distance"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
if pieces["branch"] != "master":
|
||||
rendered += ".dev0"
|
||||
rendered += plus_or_dot(pieces)
|
||||
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post.dev%d" % pieces["distance"]
|
||||
rendered = "0"
|
||||
if pieces["branch"] != "master":
|
||||
rendered += ".dev0"
|
||||
rendered += "+untagged.%d.g%s" % (pieces["distance"],
|
||||
pieces["short"])
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def pep440_split_post(ver):
|
||||
"""Split pep440 version string at the post-release segment.
|
||||
|
||||
Returns the release segments before the post-release and the
|
||||
post-release version number (or -1 if no post-release segment is present).
|
||||
"""
|
||||
vc = str.split(ver, ".post")
|
||||
return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
|
||||
|
||||
|
||||
def render_pep440_pre(pieces):
|
||||
"""TAG[.postN.devDISTANCE] -- No -dirty.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.post0.devDISTANCE
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
if pieces["distance"]:
|
||||
# update the post release segment
|
||||
tag_version, post_version = pep440_split_post(pieces["closest-tag"])
|
||||
rendered = tag_version
|
||||
if post_version is not None:
|
||||
rendered += ".post%d.dev%d" % (post_version+1, pieces["distance"])
|
||||
else:
|
||||
rendered += ".post0.dev%d" % (pieces["distance"])
|
||||
else:
|
||||
# no commits, use the tag as the version
|
||||
rendered = pieces["closest-tag"]
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post0.dev%d" % pieces["distance"]
|
||||
return rendered
|
||||
|
||||
|
||||
|
@ -380,12 +485,41 @@ def render_pep440_post(pieces):
|
|||
return rendered
|
||||
|
||||
|
||||
def render_pep440_post_branch(pieces):
|
||||
"""TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
|
||||
|
||||
The ".dev0" means not master branch.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += ".post%d" % pieces["distance"]
|
||||
if pieces["branch"] != "master":
|
||||
rendered += ".dev0"
|
||||
rendered += plus_or_dot(pieces)
|
||||
rendered += "g%s" % pieces["short"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post%d" % pieces["distance"]
|
||||
if pieces["branch"] != "master":
|
||||
rendered += ".dev0"
|
||||
rendered += "+g%s" % pieces["short"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_old(pieces):
|
||||
"""TAG[.postDISTANCE[.dev0]] .
|
||||
|
||||
The ".dev0" means dirty.
|
||||
|
||||
Eexceptions:
|
||||
Exceptions:
|
||||
1: no tags. 0.postDISTANCE[.dev0]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
|
@ -456,10 +590,14 @@ def render(pieces, style):
|
|||
|
||||
if style == "pep440":
|
||||
rendered = render_pep440(pieces)
|
||||
elif style == "pep440-branch":
|
||||
rendered = render_pep440_branch(pieces)
|
||||
elif style == "pep440-pre":
|
||||
rendered = render_pep440_pre(pieces)
|
||||
elif style == "pep440-post":
|
||||
rendered = render_pep440_post(pieces)
|
||||
elif style == "pep440-post-branch":
|
||||
rendered = render_pep440_post_branch(pieces)
|
||||
elif style == "pep440-old":
|
||||
rendered = render_pep440_old(pieces)
|
||||
elif style == "git-describe":
|
||||
|
@ -495,7 +633,7 @@ def get_versions():
|
|||
# versionfile_source is the relative path from the top of the source
|
||||
# tree (where the .git directory might live) to this file. Invert
|
||||
# this to find the root from __file__.
|
||||
for i in cfg.versionfile_source.split('/'):
|
||||
for _ in cfg.versionfile_source.split('/'):
|
||||
root = os.path.dirname(root)
|
||||
except NameError:
|
||||
return {"version": "0+unknown", "full-revisionid": None,
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
"""SatNOGS DB API hooks for drf-spectcular"""
|
||||
|
||||
EXCLUDED_PATHS = [
|
||||
'/api/satellites/{satellite_entry__norad_cat_id}/',
|
||||
]
|
||||
|
||||
|
||||
def exclude_paths_hook(endpoints):
|
||||
""" Excluding paths that are defined in EXCLUDED_PATHS list """
|
||||
return [
|
||||
(path, path_regex, method, callback) for path, path_regex, method, callback in endpoints
|
||||
if path not in EXCLUDED_PATHS
|
||||
]
|
|
@ -1,37 +1,168 @@
|
|||
"""SatNOGS DB django rest framework Filters class"""
|
||||
import django_filters
|
||||
from db.base.models import DemodData, Satellite, Transmitter
|
||||
from django_filters import Filter
|
||||
from django_filters import rest_framework as filters
|
||||
from django_filters.rest_framework import FilterSet
|
||||
|
||||
from db.base.models import SATELLITE_STATUS, Artifact, DemodData, LatestTleSet, Mode, Satellite, \
|
||||
Transmitter
|
||||
|
||||
|
||||
class ListFilter(Filter):
|
||||
"""Custom Filter to use list"""
|
||||
def filter(self, qs, value):
|
||||
"""Returns a QuerySet using list of values as input"""
|
||||
if value:
|
||||
value_list = value.replace(' ', '').split(u',')
|
||||
kwargs = {'{0}__in'.format(self.field_name): value_list}
|
||||
return qs.filter(**kwargs)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class TransmitterViewFilter(FilterSet):
|
||||
"""SatNOGS DB Transmitter API View Filter"""
|
||||
alive = filters.BooleanFilter(field_name='status', label='Alive', method='filter_status')
|
||||
mode = django_filters.ModelChoiceFilter(
|
||||
field_name='downlink_mode', lookup_expr='exact', queryset=Mode.objects.all()
|
||||
)
|
||||
|
||||
def filter_status(self, queryset, name, value):
|
||||
# see https://django-filter.readthedocs.io/en/master/ref/filters.html for
|
||||
# W0613
|
||||
def filter_status(self, queryset, name, value): # pylint: disable=W0613,R0201
|
||||
"""Returns Transmitters that are either functional or non-functional"""
|
||||
if value:
|
||||
return queryset.filter(status='functional')
|
||||
transmitters = queryset.filter(status='active')
|
||||
else:
|
||||
return queryset.exclude(status='functional')
|
||||
transmitters = queryset.exclude(status='active')
|
||||
return transmitters
|
||||
|
||||
satellite__norad_cat_id = filters.NumberFilter(
|
||||
field_name='satellite__satellite_entry__norad_cat_id', label='Satellite NORAD ID'
|
||||
)
|
||||
sat_id = django_filters.CharFilter(
|
||||
method='get_current_sat_transmitter_from_sat_id', label='Satellite ID'
|
||||
)
|
||||
|
||||
# pylint: disable=W0613,R0201
|
||||
def get_current_sat_transmitter_from_sat_id(self, queryset, field_name, value):
|
||||
"""Return the transmitter from the parent satellite in case a merged
|
||||
|
||||
satellite id is searched
|
||||
"""
|
||||
if value:
|
||||
id_list = value.replace(' ', '').split(u',')
|
||||
parent_id_list = []
|
||||
|
||||
qs = Satellite.objects.select_related('associated_satellite').filter(
|
||||
satellite_entry__approved=True, satellite_identifier__sat_id__in=id_list
|
||||
)
|
||||
|
||||
try:
|
||||
sats = qs.all()
|
||||
for sat in sats:
|
||||
if sat.associated_satellite is None:
|
||||
parent_id_list.append(sat.id)
|
||||
else:
|
||||
parent_id_list.append(sat.associated_satellite.id)
|
||||
except Satellite.DoesNotExist:
|
||||
return qs
|
||||
|
||||
return Transmitter.objects.select_related('satellite__associated_satellite').filter(
|
||||
satellite__satellite_entry__approved=True, satellite__id__in=parent_id_list
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = Transmitter
|
||||
fields = ['uuid', 'mode', 'type', 'satellite__norad_cat_id', 'alive', 'status']
|
||||
fields = [
|
||||
'uuid', 'mode', 'uplink_mode', 'type', 'satellite__norad_cat_id', 'alive', 'status',
|
||||
'service'
|
||||
]
|
||||
|
||||
|
||||
class SatelliteViewFilter(FilterSet):
|
||||
''' filter on decayed field '''
|
||||
in_orbit = filters.BooleanFilter(field_name='decayed', label='In orbit', lookup_expr='isnull')
|
||||
"""SatNOGS DB Satellite API View Filter
|
||||
|
||||
filter on decayed field
|
||||
"""
|
||||
in_orbit = filters.BooleanFilter(
|
||||
field_name='satellite_entry__decayed', label='In orbit', lookup_expr='isnull'
|
||||
)
|
||||
status = filters.ChoiceFilter(
|
||||
field_name='satellite_entry__status',
|
||||
label='Satellite Status',
|
||||
choices=list(zip(SATELLITE_STATUS, SATELLITE_STATUS))
|
||||
)
|
||||
norad_cat_id = filters.NumberFilter(
|
||||
field_name='satellite_entry__norad_cat_id', label='Satellite NORAD ID'
|
||||
)
|
||||
sat_id = django_filters.CharFilter(method='get_current_sat_from_sat_id', label='Satellite ID')
|
||||
|
||||
# pylint: disable=W0613,R0201
|
||||
def get_current_sat_from_sat_id(self, queryset, field_name, value):
|
||||
"""Return the parent Satellite in case a merged
|
||||
|
||||
satellite id is searched
|
||||
"""
|
||||
if value:
|
||||
qs = Satellite.objects.select_related('associated_satellite').filter(
|
||||
satellite_entry__approved=True, satellite_identifier__sat_id=value
|
||||
)
|
||||
try:
|
||||
sat = qs.get()
|
||||
if sat.associated_satellite is None:
|
||||
return qs
|
||||
|
||||
qs = Satellite.objects.filter(
|
||||
satellite_entry__approved=True, id=sat.associated_satellite.id
|
||||
)
|
||||
return qs
|
||||
except Satellite.DoesNotExist:
|
||||
return qs
|
||||
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = Satellite
|
||||
fields = ['norad_cat_id', 'status']
|
||||
fields = ['norad_cat_id', 'status', 'in_orbit']
|
||||
|
||||
|
||||
class TelemetryViewFilter(FilterSet):
|
||||
"""SatNOGS DB Telemetry API View Filter"""
|
||||
satellite = django_filters.NumberFilter(
|
||||
field_name='satellite__norad_cat_id', lookup_expr='exact'
|
||||
field_name='satellite__satellite_entry__norad_cat_id',
|
||||
lookup_expr='exact',
|
||||
label='Satellite NORAD ID'
|
||||
)
|
||||
sat_id = ListFilter(field_name='satellite__satellite_identifier__sat_id', label='Satellite ID')
|
||||
start = django_filters.IsoDateTimeFilter(field_name='timestamp', lookup_expr='gte')
|
||||
end = django_filters.IsoDateTimeFilter(field_name='timestamp', lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = DemodData
|
||||
fields = ['satellite']
|
||||
fields = ['satellite', 'app_source', 'observer', 'transmitter']
|
||||
|
||||
|
||||
class LatestTleSetViewFilter(FilterSet):
|
||||
"""SatNOGS DB LatestTleSet API View Filter"""
|
||||
norad_cat_id = django_filters.NumberFilter(
|
||||
field_name='satellite__satellite_entry__norad_cat_id',
|
||||
lookup_expr='exact',
|
||||
label='Satellite NORAD ID'
|
||||
)
|
||||
sat_id = ListFilter(field_name='satellite__satellite_identifier__sat_id', label='Satellite ID')
|
||||
|
||||
class Meta:
|
||||
model = LatestTleSet
|
||||
fields = ['norad_cat_id']
|
||||
|
||||
|
||||
class ArtifactViewFilter(FilterSet):
|
||||
"""SatNOGS DB Artifact API View Filter"""
|
||||
class Meta:
|
||||
model = Artifact
|
||||
fields = [
|
||||
'network_obs_id',
|
||||
]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""
|
||||
Custom pagination classes for REST framework
|
||||
"""
|
||||
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
@ -18,14 +17,13 @@ class LinkedHeaderPageNumberPagination(PageNumberPagination):
|
|||
next_url = self.get_next_link()
|
||||
previous_url = self.get_previous_link()
|
||||
|
||||
link = ''
|
||||
if next_url is not None and previous_url is not None:
|
||||
link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"'
|
||||
elif next_url is not None:
|
||||
link = '<{next_url}>; rel="next"'
|
||||
elif previous_url is not None:
|
||||
link = '<{previous_url}>; rel="prev"'
|
||||
else:
|
||||
link = ''
|
||||
link = link.format(next_url=next_url, previous_url=previous_url)
|
||||
headers = {'Link': link} if link else {}
|
||||
return Response(data, headers=headers)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
"""SatNOGS DB django rest framework API custom parsers"""
|
||||
from pyld import jsonld
|
||||
from rest_framework.parsers import JSONParser
|
||||
|
||||
from db.base.structured_data import get_structured_data
|
||||
|
||||
|
||||
class JSONLDParser(JSONParser): # pylint: disable=R0903
|
||||
""" Parser for JSONLD. """
|
||||
|
||||
media_type = 'application/ld+json'
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None):
|
||||
""" Render `data` into JSONLD, returning a bytestring. """
|
||||
raw_data = super().parse(stream, media_type, parser_context)
|
||||
structured_data = get_structured_data(parser_context['view'].basename, [])
|
||||
data = jsonld.frame(raw_data, structured_data.frame, {'omitGraph': False})
|
||||
return data
|
|
@ -0,0 +1,23 @@
|
|||
"""SatNOGS DB API permissions, django rest framework"""
|
||||
from rest_framework import permissions
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
|
||||
class SafeMethodsWithPermission(permissions.BasePermission):
|
||||
"""Access non-destructive methods (like GET and HEAD) with API Key"""
|
||||
def has_permission(self, request, view):
|
||||
return self.has_object_permission(request, view)
|
||||
|
||||
def has_object_permission(self, request, view, obj=None):
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return request.user.is_authenticated
|
||||
return True
|
||||
|
||||
|
||||
class IsAuthenticatedOrOptions(IsAuthenticated):
|
||||
"""Allow unauthenticated access for OPTIONS method,
|
||||
check authentication for all other methods."""
|
||||
def has_permission(self, request, view):
|
||||
if request.method == 'OPTIONS':
|
||||
return True
|
||||
return super().has_permission(request, view)
|
|
@ -0,0 +1,28 @@
|
|||
"""SatNOGS DB django rest framework API custom renderers"""
|
||||
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
|
||||
|
||||
from db.base.structured_data import get_structured_data
|
||||
|
||||
|
||||
class JSONLDRenderer(JSONRenderer):
|
||||
""" Renderer which serializes to JSONLD. """
|
||||
|
||||
media_type = 'application/ld+json'
|
||||
format = 'json-ld'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
""" Render `data` into JSONLD, returning a bytestring. """
|
||||
if renderer_context['response'].exception:
|
||||
return super().render(data, accepted_media_type, renderer_context)
|
||||
|
||||
structured_data = get_structured_data(renderer_context['view'].basename, data)
|
||||
jsonld = structured_data.get_jsonld()
|
||||
return super().render(jsonld, accepted_media_type, renderer_context)
|
||||
|
||||
|
||||
class BrowserableJSONLDRenderer(BrowsableAPIRenderer):
|
||||
""" Renderer for Browserable API with JSONLD format. """
|
||||
format = 'browse-json-ld'
|
||||
|
||||
def get_default_renderer(self, view):
|
||||
return JSONLDRenderer()
|
|
@ -1,92 +1,561 @@
|
|||
from db.base.models import TRANSMITTER_STATUS, DemodData, Mode, Satellite, \
|
||||
Transmitter
|
||||
"""SatNOGS DB API serializers, django rest framework"""
|
||||
# pylint: disable=R0201
|
||||
|
||||
import h5py
|
||||
from django.utils.datastructures import MultiValueDictKeyError
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiExample, extend_schema_field, extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from db.base.models import TRANSMITTER_STATUS, Artifact, DemodData, LatestTleSet, Mode, \
|
||||
Satellite, SatelliteEntry, Telemetry, Transmitter, TransmitterEntry
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Mode Example 1',
|
||||
summary='Example: list all modes',
|
||||
description='This is a truncated example response for listing all RF Mode entries',
|
||||
value=[
|
||||
{
|
||||
'id': 49,
|
||||
'name': 'AFSK'
|
||||
},
|
||||
],
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class ModeSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB Mode API Serializer"""
|
||||
class Meta:
|
||||
model = Mode
|
||||
fields = ('id', 'name')
|
||||
|
||||
|
||||
class SatTelemetrySerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB satellite telemetry API Serializer"""
|
||||
class Meta:
|
||||
model = Telemetry
|
||||
fields = ['decoder']
|
||||
|
||||
|
||||
class SatelliteEntrySerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB SatelliteEntry API Serializer"""
|
||||
class Meta:
|
||||
model = SatelliteEntry
|
||||
fields = (
|
||||
'satellite_identifier', 'norad_cat_id', 'name', 'names', 'status', 'citation',
|
||||
'created_by'
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Satellite Example 1',
|
||||
summary='Example: retrieving ISS',
|
||||
description='This is an example response for retrieving the ISS entry, NORAD ID 25544',
|
||||
value={
|
||||
'norad_cat_id': 25544,
|
||||
'name': 'ISS',
|
||||
'names': 'ZARYA',
|
||||
'image': 'https://db-satnogs.freetls.fastly.net/media/satellites/ISS.jpg',
|
||||
'status': 'alive',
|
||||
'decayed': None,
|
||||
'launched': '1998-11-20T00:00:00Z',
|
||||
'deployed': '1998-11-20T00:00:00Z',
|
||||
'website': 'https://www.nasa.gov/mission_pages/station/main/index.html',
|
||||
'operator': 'None',
|
||||
'countries': 'RU,US',
|
||||
'telemetries': [{
|
||||
'decoder': 'iss'
|
||||
}]
|
||||
},
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class SatelliteSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB Satellite API Serializer"""
|
||||
|
||||
sat_id = serializers.SerializerMethodField()
|
||||
norad_cat_id = serializers.SerializerMethodField()
|
||||
norad_follow_id = serializers.SerializerMethodField()
|
||||
name = serializers.SerializerMethodField()
|
||||
names = serializers.SerializerMethodField()
|
||||
image = serializers.SerializerMethodField()
|
||||
status = serializers.SerializerMethodField()
|
||||
decayed = serializers.SerializerMethodField()
|
||||
launched = serializers.SerializerMethodField()
|
||||
deployed = serializers.SerializerMethodField()
|
||||
website = serializers.SerializerMethodField()
|
||||
operator = serializers.SerializerMethodField()
|
||||
countries = serializers.SerializerMethodField()
|
||||
telemetries = serializers.SerializerMethodField()
|
||||
updated = serializers.SerializerMethodField()
|
||||
citation = serializers.SerializerMethodField()
|
||||
associated_satellites = serializers.SerializerMethodField()
|
||||
operator = serializers.SerializerMethodField()
|
||||
is_frequency_violator = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Satellite
|
||||
fields = ('norad_cat_id', 'name', 'names', 'image', 'status', 'decayed')
|
||||
fields = (
|
||||
'sat_id', 'norad_cat_id', 'norad_follow_id', 'name', 'names', 'image', 'status',
|
||||
'decayed', 'launched', 'deployed', 'website', 'operator', 'countries', 'telemetries',
|
||||
'updated', 'citation', 'is_frequency_violator', 'associated_satellites'
|
||||
)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_sat_id(self, obj):
|
||||
"""Returns Satellite sat_id"""
|
||||
return obj.satellite_identifier.sat_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
def get_norad_cat_id(self, obj):
|
||||
"""Returns Satellite norad_cat_id"""
|
||||
return obj.satellite_entry.norad_cat_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
def get_norad_follow_id(self, obj):
|
||||
"""Returns Satellite norad_follow_id"""
|
||||
return obj.satellite_entry.norad_follow_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_name(self, obj):
|
||||
"""Returns Satellite name"""
|
||||
return obj.satellite_entry.name
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_names(self, obj):
|
||||
"""Returns Satellite alternative names"""
|
||||
return obj.satellite_entry.names
|
||||
|
||||
@extend_schema_field(OpenApiTypes.URI)
|
||||
def get_image(self, obj):
|
||||
"""Returns Satellite image URI"""
|
||||
return str(obj.satellite_entry.image)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_status(self, obj):
|
||||
"""Returns Satellite status text"""
|
||||
return obj.satellite_entry.status
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||
def get_decayed(self, obj):
|
||||
"""Returns Satellite decayed datetime"""
|
||||
return obj.satellite_entry.decayed
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||
def get_launched(self, obj):
|
||||
"""Returns Satellite launched datetime"""
|
||||
return obj.satellite_entry.launched
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||
def get_deployed(self, obj):
|
||||
"""Returns Satellite deployed datetime"""
|
||||
return obj.satellite_entry.deployed
|
||||
|
||||
@extend_schema_field(OpenApiTypes.URI)
|
||||
def get_website(self, obj):
|
||||
"""Returns Satellite website"""
|
||||
return obj.satellite_entry.website
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_operator(self, obj):
|
||||
"""Returns operator text"""
|
||||
return str(obj.satellite_entry.operator)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_countries(self, obj):
|
||||
"""Returns countires"""
|
||||
return obj.satellite_entry.countries_str
|
||||
|
||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||
def get_telemetries(self, obj):
|
||||
"""Returns telemetries"""
|
||||
telemetries = SatTelemetrySerializer(obj.telemetries, many=True, read_only=True)
|
||||
return telemetries.data
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||
def get_updated(self, obj):
|
||||
"""Returns Satellite decayed datetime"""
|
||||
return obj.satellite_entry.reviewed
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_citation(self, obj):
|
||||
"""Returns Satellite decayed datetime"""
|
||||
return obj.satellite_entry.citation
|
||||
|
||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||
def get_associated_satellites(self, obj):
|
||||
"""Returns Satellite IDs that are associated with the Satellite"""
|
||||
return [
|
||||
merged_satellite.satellite_identifier.sat_id
|
||||
for merged_satellite in obj.associated_with.all()
|
||||
]
|
||||
|
||||
@extend_schema_field(OpenApiTypes.BOOL)
|
||||
def get_is_frequency_violator(self, obj):
|
||||
"""Returns if there is a frequency violation"""
|
||||
return obj.has_bad_transmitter
|
||||
|
||||
|
||||
class TransmitterEntrySerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB TransmitterEntry API Serializer"""
|
||||
class Meta:
|
||||
model = TransmitterEntry
|
||||
fields = (
|
||||
'uuid', 'description', 'status', 'type', 'uplink_low', 'uplink_high', 'uplink_drift',
|
||||
'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode', 'uplink_mode',
|
||||
'invert', 'baud', 'satellite', 'citation', 'service', 'iaru_coordination',
|
||||
'iaru_coordination_url', 'itu_notification', 'created_by'
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Transmitter Example 1',
|
||||
summary='Example: Transmitter API response',
|
||||
value={
|
||||
'uuid': 'eozSf5mKyzNxoascs8V4bV',
|
||||
'description': 'Mode V/U FM - Voice Repeater',
|
||||
'alive': True,
|
||||
'type': 'Transceiver',
|
||||
'uplink_low': 145990000,
|
||||
'uplink_high': None,
|
||||
'uplink_drift': None,
|
||||
'downlink_low': 437800000,
|
||||
'downlink_high': None,
|
||||
'downlink_drift': None,
|
||||
'mode': 'FM',
|
||||
'mode_id': 1,
|
||||
'uplink_mode': 'FM',
|
||||
'invert': False,
|
||||
'baud': None,
|
||||
'norad_cat_id': 25544,
|
||||
'status': 'active',
|
||||
'updated': '2020-09-03T13:14:41.552071Z',
|
||||
'citation': 'https://www.ariss.org/press-releases/september-2-2020',
|
||||
'service': 'Amateur',
|
||||
'iaru_coordination': '',
|
||||
'iaru_coordination_url': '',
|
||||
'itu_notification': ''
|
||||
},
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class TransmitterSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB Transmitter API Serializer"""
|
||||
sat_id = serializers.SerializerMethodField()
|
||||
norad_cat_id = serializers.SerializerMethodField()
|
||||
mode_id = serializers.SerializerMethodField()
|
||||
norad_follow_id = serializers.SerializerMethodField()
|
||||
mode = serializers.SerializerMethodField()
|
||||
mode_id = serializers.SerializerMethodField()
|
||||
uplink_mode = serializers.SerializerMethodField()
|
||||
alive = serializers.SerializerMethodField()
|
||||
updated = serializers.DateTimeField(source='created')
|
||||
updated = serializers.DateTimeField(source='reviewed')
|
||||
frequency_violation = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Transmitter
|
||||
fields = (
|
||||
'uuid', 'description', 'alive', 'type', 'uplink_low', 'uplink_high', 'uplink_drift',
|
||||
'downlink_low', 'downlink_high', 'downlink_drift', 'mode_id', 'mode', 'invert', 'baud',
|
||||
'norad_cat_id', 'status', 'updated', 'citation'
|
||||
'downlink_low', 'downlink_high', 'downlink_drift', 'mode', 'mode_id', 'uplink_mode',
|
||||
'invert', 'baud', 'sat_id', 'norad_cat_id', 'norad_follow_id', 'status', 'updated',
|
||||
'citation', 'service', 'iaru_coordination', 'iaru_coordination_url',
|
||||
'itu_notification', 'frequency_violation'
|
||||
)
|
||||
|
||||
# Keeping alive field for compatibility issues
|
||||
@extend_schema_field(OpenApiTypes.BOOL)
|
||||
def get_alive(self, obj):
|
||||
"""Returns transmitter status"""
|
||||
return obj.status == TRANSMITTER_STATUS[0]
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
def get_mode_id(self, obj):
|
||||
"""Returns downlink mode id"""
|
||||
try:
|
||||
return obj.mode.id
|
||||
except Exception:
|
||||
return obj.downlink_mode.id
|
||||
except AttributeError: # rare chance that this happens in prod
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
def get_mode(self, obj):
|
||||
"""Returns downlink mode name"""
|
||||
try:
|
||||
return obj.mode.name
|
||||
except Exception:
|
||||
return obj.downlink_mode.name
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
def get_uplink_mode(self, obj):
|
||||
"""Returns uplink mode name"""
|
||||
try:
|
||||
return obj.uplink_mode.name
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
def get_norad_cat_id(self, obj):
|
||||
return obj.satellite.norad_cat_id
|
||||
"""Returns Satellite NORAD ID"""
|
||||
try:
|
||||
return obj.satellite.satellite_entry.norad_cat_id
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
def get_norad_follow_id(self, obj):
|
||||
"""Returns Satellite NORAD ID following initial determination"""
|
||||
try:
|
||||
return obj.satellite.satellite_entry.norad_follow_id
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_sat_id(self, obj):
|
||||
"""Returns Satellite NORAD ID"""
|
||||
try:
|
||||
return obj.satellite.satellite_identifier.sat_id
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@extend_schema_field(OpenApiTypes.BOOL)
|
||||
def get_frequency_violation(self, obj):
|
||||
"""Returns if there is a frequency violation"""
|
||||
return obj.bad_transmitter
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'TLE Example 1',
|
||||
summary='Example: TLE API response',
|
||||
value={
|
||||
'tle0': '0 ISS (ZARYA)',
|
||||
'tle1': '1 25544U 98067A 21009.90234038 .00001675 00000-0 38183-4 0 9997',
|
||||
'tle2': '2 25544 51.6464 45.6388 0000512 205.3232 213.2158 15.49275327264062',
|
||||
'tle_source': 'undisclosed',
|
||||
'norad_cat_id': 25544,
|
||||
'updated': '2021-01-09T22:46:37.781923+0000'
|
||||
},
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class LatestTleSetSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB LatestTleSet API Serializer"""
|
||||
|
||||
sat_id = serializers.SerializerMethodField()
|
||||
norad_cat_id = serializers.SerializerMethodField()
|
||||
tle0 = serializers.SerializerMethodField()
|
||||
tle1 = serializers.SerializerMethodField()
|
||||
tle2 = serializers.SerializerMethodField()
|
||||
tle_source = serializers.SerializerMethodField()
|
||||
updated = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = LatestTleSet
|
||||
fields = ('tle0', 'tle1', 'tle2', 'tle_source', 'sat_id', 'norad_cat_id', 'updated')
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_sat_id(self, obj):
|
||||
"""Returns Satellite Satellite Identifier"""
|
||||
return obj.satellite.satellite_identifier.sat_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
def get_norad_cat_id(self, obj):
|
||||
"""Returns Satellite NORAD ID"""
|
||||
return obj.satellite.satellite_entry.norad_cat_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_tle0(self, obj):
|
||||
"""Returns TLE line 0"""
|
||||
return obj.tle0
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_tle1(self, obj):
|
||||
"""Returns TLE line 1"""
|
||||
return obj.tle1
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_tle2(self, obj):
|
||||
"""Returns TLE line 2"""
|
||||
return obj.tle2
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_tle_source(self, obj):
|
||||
"""Returns TLE source"""
|
||||
return obj.tle_source
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||
def get_updated(self, obj):
|
||||
"""Returns TLE updated datetime"""
|
||||
return obj.updated.strftime('%Y-%m-%dT%H:%M:%S.%f%z')
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('app_source', 'observer', 'timestamp'),
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'Telemetry Example 1',
|
||||
summary='Example: retrieving a single Telemetry frame',
|
||||
description='This is an example response for retrieving a single data frame',
|
||||
value={
|
||||
'norad_cat_id': 40379,
|
||||
'transmitter': None,
|
||||
'app_source': 'network',
|
||||
'decoded': 'influxdb',
|
||||
'frame': '968870A6A0A66086A240404040E103F0ABCD0000004203F500B475E215EA5FA0040C000B'
|
||||
'000900010025008E55EE7B64650100000000AE4D07005D660F007673340000C522370067076507FD0'
|
||||
'C60002700FE0CC50E0D00AD0E0B069007BD0E0E00650D21001400FE0C910054007007690D8700FC0C'
|
||||
'BA00E40743001C0F140077077807D7078E00120F240068076D07DA0A74003D0F2500830780077A0AC'
|
||||
'401490F960070077207FDFC9F079507950700C03B0015009AFF6900C8FFE0FFA700EBFF3A00F200F3'
|
||||
'FF02016D0A590A0D0AE3099B0C830CB50DA70D9D06CC0043009401B8338B334C20001000000000009'
|
||||
'F02000003000000FF723D00BEFFFFFFFF2E89B0151C00',
|
||||
'observer': 'KB9JHU-EM69uf',
|
||||
'timestamp': '2021-01-05T22:28:09Z'
|
||||
},
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class TelemetrySerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB Telemetry API Serializer"""
|
||||
sat_id = serializers.SerializerMethodField()
|
||||
norad_cat_id = serializers.SerializerMethodField()
|
||||
transmitter = serializers.SerializerMethodField()
|
||||
schema = serializers.SerializerMethodField()
|
||||
decoded = serializers.SerializerMethodField()
|
||||
frame = serializers.SerializerMethodField()
|
||||
associated_satellites = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = DemodData
|
||||
fields = (
|
||||
'norad_cat_id', 'transmitter', 'app_source', 'schema', 'decoded', 'frame', 'observer',
|
||||
'timestamp'
|
||||
'sat_id', 'norad_cat_id', 'transmitter', 'app_source', 'decoded', 'frame', 'observer',
|
||||
'timestamp', 'version', 'observation_id', 'station_id', 'associated_satellites'
|
||||
)
|
||||
|
||||
def get_norad_cat_id(self, obj):
|
||||
return obj.satellite.norad_cat_id
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_sat_id(self, obj):
|
||||
"""Returns Satellite Identifier"""
|
||||
if obj.satellite.associated_satellite:
|
||||
return obj.satellite.associated_satellite.satellite_identifier.sat_id
|
||||
return obj.satellite.satellite_identifier.sat_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT64)
|
||||
def get_norad_cat_id(self, obj):
|
||||
"""Returns Satellite NORAD ID"""
|
||||
return obj.satellite.satellite_entry.norad_cat_id
|
||||
|
||||
@extend_schema_field(OpenApiTypes.UUID)
|
||||
def get_transmitter(self, obj):
|
||||
"""Returns Transmitter UUID"""
|
||||
try:
|
||||
return obj.transmitter.uuid
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def get_schema(self, obj):
|
||||
try:
|
||||
return obj.payload_telemetry.schema
|
||||
except Exception:
|
||||
except AttributeError:
|
||||
return ''
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_decoded(self, obj):
|
||||
"""Returns the payload_decoded field"""
|
||||
return obj.payload_decoded
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_frame(self, obj):
|
||||
"""Returns the payload frame"""
|
||||
return obj.display_frame()
|
||||
|
||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||
def get_associated_satellites(self, obj):
|
||||
"""Returns Satellite IDs that are associated with the Satellite"""
|
||||
satellite = obj.satellite
|
||||
if satellite.associated_satellite:
|
||||
satellite = satellite.associated_satellite
|
||||
return [
|
||||
merged_satellite.satellite_identifier.sat_id
|
||||
for merged_satellite in satellite.associated_with.all()
|
||||
]
|
||||
|
||||
# @extend_schema_field(OpenApiTypes.STR)
|
||||
# def get_version(self, obj):
|
||||
# """Returns the payload version"""
|
||||
# return obj.version
|
||||
|
||||
|
||||
class SidsSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB SiDS API Serializer"""
|
||||
class Meta:
|
||||
model = DemodData
|
||||
fields = ('satellite', 'payload_frame', 'station', 'lat', 'lng', 'timestamp', 'app_source')
|
||||
fields = (
|
||||
'satellite', 'payload_frame', 'station', 'lat', 'lng', 'timestamp', 'app_source',
|
||||
'observer', 'version', 'observation_id', 'station_id'
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'View Artifact Example 1',
|
||||
summary='Example: retrieving a specific artifact',
|
||||
description='This is an example response when requesting a specific artifact '
|
||||
'previously uploaded to DB',
|
||||
value={
|
||||
'id': 1337,
|
||||
'network_obs_id': 3376466,
|
||||
'artifact_file': 'http://db-dev.satnogs.org/media/artifacts/bba35b2d-76cc-4a8f-'
|
||||
'9b8a-4a2ecb09c6df.h5'
|
||||
},
|
||||
status_codes=['200'],
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class ArtifactSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS DB Artifacts API Serializer"""
|
||||
class Meta:
|
||||
model = Artifact
|
||||
fields = ('id', 'network_obs_id', 'artifact_file')
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
'New Artifact Example 1',
|
||||
summary='Example: uploading artifact',
|
||||
description='This is an example response after successfully uploading an artifact '
|
||||
'file. The ID of the artifact is returned',
|
||||
value={
|
||||
'id': 1337,
|
||||
},
|
||||
status_codes=['200', '201'],
|
||||
response_only=True, # signal that example only applies to responses
|
||||
),
|
||||
]
|
||||
)
|
||||
class NewArtifactSerializer(serializers.ModelSerializer):
|
||||
"""SatNOGS Network New Artifact API Serializer"""
|
||||
def validate(self, attrs):
|
||||
"""Validates data of incoming artifact"""
|
||||
|
||||
try:
|
||||
with h5py.File(self.initial_data['artifact_file'], 'r') as h5_file:
|
||||
if 'artifact_version' not in h5_file.attrs:
|
||||
raise serializers.ValidationError(
|
||||
'Not a valid SatNOGS Artifact.', code='invalid'
|
||||
)
|
||||
except (OSError, MultiValueDictKeyError) as error:
|
||||
raise serializers.ValidationError(
|
||||
'Not a valid HDF5 file: {}'.format(error), code='invalid'
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
model = Artifact
|
||||
fields = ('artifact_file', )
|
||||
|
|
189
db/api/tests.py
189
db/api/tests.py
|
@ -1,10 +1,11 @@
|
|||
"""SatNOGS DB API test suites"""
|
||||
import pytest
|
||||
from django.contrib.auth.models import User # pylint: disable=E5142
|
||||
from django.test import TestCase
|
||||
|
||||
from db.base.tests import DemodDataFactory, ModeFactory, SatelliteFactory, \
|
||||
TransmitterFactory
|
||||
from rest_framework import status
|
||||
|
||||
from db.base.tests import DemodDataFactory, ModeFactory, SatelliteFactory, TransmitterFactory
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
class ModeViewApiTest(TestCase):
|
||||
|
@ -18,10 +19,12 @@ class ModeViewApiTest(TestCase):
|
|||
self.mode.save()
|
||||
|
||||
def test_list(self):
|
||||
"""Test the API modes list"""
|
||||
response = self.client.get('/api/modes/', format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_retrieve(self):
|
||||
"""Test the API mode retrieval"""
|
||||
response = self.client.get('/api/modes/{0}/'.format(self.mode.id), format='json')
|
||||
self.assertContains(response, self.mode.name)
|
||||
|
||||
|
@ -38,14 +41,40 @@ class SatelliteViewApiTest(TestCase):
|
|||
self.satellite.save()
|
||||
|
||||
def test_list(self):
|
||||
"""Test the Satellite API listing"""
|
||||
response = self.client.get('/api/satellites/', format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_retrieve(self):
|
||||
def test_retrieve_with_norad_id(self):
|
||||
"""Test the Satellite API retrieval with NORAD ID"""
|
||||
response = self.client.get(
|
||||
'/api/satellites/{0}/'.format(self.satellite.norad_cat_id), format='json'
|
||||
'/api/satellites/{0}/'.format(self.satellite.satellite_entry.norad_cat_id),
|
||||
format='json'
|
||||
)
|
||||
self.assertContains(response, self.satellite.name)
|
||||
self.assertContains(response, self.satellite.satellite_entry.name)
|
||||
|
||||
def test_retrieve_with_satellite_id(self):
|
||||
"""Test the Satellite API retrieval with Satellite Identifier"""
|
||||
response = self.client.get(
|
||||
'/api/satellites/{0}/'.format(self.satellite.satellite_identifier.sat_id),
|
||||
format='json'
|
||||
)
|
||||
self.assertContains(response, self.satellite.satellite_entry.name)
|
||||
|
||||
def test_retrieve_nonexistent_satellite(self):
|
||||
"""Tests for a non existent satellite"""
|
||||
response = self.client.get('/api/satellites/{0}/'.format('BADBADBADBAD'), format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_retrieve_jsonld_satellites(self):
|
||||
"""Tests the return of a satellite via JSONLD browsable renderer"""
|
||||
response = self.client.get('/api/satellites/?format=json-ld')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_retrieve_browse_jsonld_satellites(self):
|
||||
"""Tests the return of a satellite via JSONLD browsable renderer"""
|
||||
response = self.client.get('/api/satellites/?format=browse-json-ld')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
|
@ -56,15 +85,28 @@ class TransmitterViewApiTest(TestCase):
|
|||
transmitter = None
|
||||
|
||||
def setUp(self):
|
||||
self.transmitter = TransmitterFactory()
|
||||
TransmitterFactory.create_batch(size=50)
|
||||
self.transmitter = TransmitterFactory(status='active')
|
||||
self.transmitter.uuid = 'test'
|
||||
self.transmitter.save()
|
||||
|
||||
def test_list(self):
|
||||
"""Test the Transmitter API listing"""
|
||||
response = self.client.get('/api/transmitters/', format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_list_active(self):
|
||||
"""Test the Transmitter API listing with active filter"""
|
||||
response = self.client.get('/api/transmitters/?Alive=true&format=json')
|
||||
self.assertContains(response, '\"active\"')
|
||||
|
||||
def test_list_inactive(self):
|
||||
"""Test the Transmitter API listing with inactive"""
|
||||
response = self.client.get('/api/transmitters/?Alive=false&format=json')
|
||||
self.assertContains(response, '\"inactive\"')
|
||||
|
||||
def test_retrieve(self):
|
||||
"""Test the Transmitter API retrieval"""
|
||||
response = self.client.get(
|
||||
'/api/transmitters/{0}/'.format(self.transmitter.uuid), format='json'
|
||||
)
|
||||
|
@ -72,20 +114,145 @@ class TransmitterViewApiTest(TestCase):
|
|||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.usefixtures('celery_session_app')
|
||||
@pytest.mark.usefixtures('celery_session_worker')
|
||||
class TelemetryViewApiTest(TestCase):
|
||||
"""
|
||||
Tests the Telemetry View API
|
||||
"""
|
||||
datum = None
|
||||
satellite = None
|
||||
|
||||
frame = '60A060A0A46E609C8262A6A640E082A0A4A682A86103F02776261C6C201C5'
|
||||
frame += '3495D41524953532D496E7465726E6174696F6E616C2053706163652053746174696F6E3D0D'
|
||||
|
||||
def setUp(self):
|
||||
self.datum = DemodDataFactory()
|
||||
self.datum.save()
|
||||
self.satellite = SatelliteFactory()
|
||||
self.satellite.save()
|
||||
|
||||
def test_list(self):
|
||||
response = self.client.get('/api/telemetry/', format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
def test_list_anonymous(self):
|
||||
"""Test the Telemetry API listing"""
|
||||
response = self.client.get('/api/telemetry/')
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_retrieve(self):
|
||||
"""Test the Telemetry API retrieval"""
|
||||
response = self.client.get('/api/telemetry/{0}/'.format(self.datum.id), format='json')
|
||||
self.assertContains(response, self.datum.observer)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_post(self):
|
||||
"""Test the network posting capability"""
|
||||
norad = self.satellite.satellite_entry.norad_cat_id
|
||||
|
||||
data = {
|
||||
'frame': self.frame,
|
||||
'locator': 'longLat',
|
||||
'latitude': '06.12S',
|
||||
'longitude': '59.34W',
|
||||
'noradID': str(norad),
|
||||
'source': 'T3ST',
|
||||
'timestamp': '2021-03-15T13:14:04.940Z',
|
||||
'version': '1.2.3',
|
||||
'observation_id': '123456789',
|
||||
'satnogs_network': 'true',
|
||||
'station_id': '2'
|
||||
}
|
||||
response = self.client.post('/api/telemetry/', data=data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_post_numerical_latlng(self):
|
||||
"""Test the SiDS posting capability without a N/S and E/W identifier"""
|
||||
norad = self.satellite.satellite_entry.norad_cat_id
|
||||
|
||||
data = {
|
||||
'frame': self.frame,
|
||||
'locator': 'longLat',
|
||||
'latitude': '06.12',
|
||||
'longitude': '59.34',
|
||||
'noradID': str(norad),
|
||||
'source': 'T3ST',
|
||||
'timestamp': '2021-03-15T13:14:04.940Z'
|
||||
}
|
||||
response = self.client.post('/api/telemetry/', data=data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_post_new_satellite(self):
|
||||
"""Test the SiDS posting capability while creating a new satellite"""
|
||||
|
||||
data = {
|
||||
'frame': self.frame,
|
||||
'locator': 'longLat',
|
||||
'latitude': '06.12S',
|
||||
'longitude': '59.34W',
|
||||
'noradID': '999999',
|
||||
'source': 'T3ST',
|
||||
'timestamp': '2021-03-15T13:14:04.940Z',
|
||||
'version': '1.2.3'
|
||||
}
|
||||
response = self.client.post('/api/telemetry/', data=data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_post_bad_new_satellite(self):
|
||||
"""Test the SiDS upload while creating a new satellite with bad NORAD"""
|
||||
|
||||
data = {
|
||||
'frame': self.frame,
|
||||
'locator': 'longLat',
|
||||
'latitude': '06.12S',
|
||||
'longitude': '59.34W',
|
||||
'noradID': 'STR999999',
|
||||
'source': 'T3ST',
|
||||
'timestamp': '2021-03-15T13:14:04.940Z',
|
||||
'version': '1.2.3'
|
||||
}
|
||||
response = self.client.post('/api/telemetry/', data=data)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_bad_post(self):
|
||||
"""Test the SiDS posting capability with bad data"""
|
||||
norad = self.satellite.satellite_entry.norad_cat_id
|
||||
|
||||
data = {
|
||||
'frame': '',
|
||||
'locator': 'longLat',
|
||||
'latitude': '206.12S',
|
||||
'longitude': '59.34WE',
|
||||
'noradID': str(norad),
|
||||
'source': '',
|
||||
'timestamp': ''
|
||||
}
|
||||
response = self.client.post('/api/telemetry/', data=data)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
class LoginView(TestCase):
|
||||
"""
|
||||
Tests various API endpoints with authentication
|
||||
"""
|
||||
datum = None
|
||||
|
||||
def setUp(self):
|
||||
DemodDataFactory.create_batch(size=18)
|
||||
self.datum = DemodDataFactory()
|
||||
self.datum.save()
|
||||
self.client.force_login(User.objects.get_or_create(username='testuser')[0])
|
||||
|
||||
def test_auth_telemetry_list_without_filter(self):
|
||||
"""Test the Telemetry API listing and pagination with authentication"""
|
||||
response = self.client.get('/api/telemetry/?page=1')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_auth_telemetry_list_with_satellite_filter(self):
|
||||
"""Test the Telemetry API listing and pagination with authentication"""
|
||||
norad_id = self.datum.satellite.satellite_entry.norad_cat_id
|
||||
response = self.client.get('/api/telemetry/?page=1&satellite=' + str(norad_id))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_auth_telemetry_list_with_sat_id_filter(self):
|
||||
"""Test the Telemetry API listing and pagination with authentication"""
|
||||
sat_id = self.datum.satellite.satellite_identifier.sat_id
|
||||
response = self.client.get('/api/telemetry/?page=1&sat_id=' + sat_id)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
"""SatNOGS DB API throttling classes, django rest framework"""
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import throttling
|
||||
|
||||
from db.base.models import Satellite
|
||||
|
||||
|
||||
class GetTelemetryAnononymousRateThrottle(throttling.AnonRateThrottle):
|
||||
"""Anonymous GET Throttling for Telemetry API endpoint"""
|
||||
scope = 'get_telemetry_anon'
|
||||
|
||||
def allow_request(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return True
|
||||
return super().allow_request(request, view)
|
||||
|
||||
|
||||
class GetTelemetryUserRateThrottle(throttling.UserRateThrottle):
|
||||
"""User GET Throttling for Telemetry API endpoint"""
|
||||
scope = 'get_telemetry_user'
|
||||
|
||||
def allow_request(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return True
|
||||
return super().allow_request(request, view)
|
||||
|
||||
|
||||
class GetTelemetryViolatorThrottle(throttling.BaseThrottle):
|
||||
"""Violator satellites GET Throttling for Telemetry API endpoint"""
|
||||
scope = 'get_telemetry_violator'
|
||||
|
||||
def allow_request(self, request, view):
|
||||
if request.method == 'POST':
|
||||
return True
|
||||
satellite = request.query_params.get('satellite', None)
|
||||
sat_id = request.query_params.get('sat_id', None)
|
||||
violation = None
|
||||
|
||||
if sat_id:
|
||||
violation = cache.get('violator_' + sat_id)
|
||||
elif satellite:
|
||||
violation = cache.get('violator_' + str(satellite))
|
||||
else:
|
||||
return True
|
||||
|
||||
if violation is None:
|
||||
if sat_id:
|
||||
satellite_obj = get_object_or_404(Satellite, satellite_identifier__sat_id=sat_id)
|
||||
else:
|
||||
satellite_obj = get_object_or_404(
|
||||
Satellite, satellite_entry__norad_cat_id=satellite
|
||||
)
|
||||
if satellite_obj.associated_satellite:
|
||||
satellite_obj = satellite_obj.associated_satellite
|
||||
if satellite_obj.has_bad_transmitter:
|
||||
return cache.add('violator_telemetry_' + str(satellite_obj.id), True, 86400)
|
||||
elif violation['status']:
|
||||
return cache.add('violator_telemetry_' + str(violation['id']), True, 86400)
|
||||
return True
|
|
@ -1,11 +1,24 @@
|
|||
from db.api import views
|
||||
"""SatNOGS DB django rest framework API url routings"""
|
||||
from django.urls import include, path
|
||||
from rest_framework import routers
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
from db.api import views
|
||||
|
||||
router.register(r'modes', views.ModeView)
|
||||
router.register(r'satellites', views.SatelliteView)
|
||||
router.register(r'transmitters', views.TransmitterView)
|
||||
router.register(r'telemetry', views.TelemetryView)
|
||||
ROUTER = routers.DefaultRouter()
|
||||
|
||||
api_urlpatterns = router.urls
|
||||
ROUTER.register(r'artifacts', views.ArtifactViewSet)
|
||||
ROUTER.register(r'modes', views.ModeViewSet)
|
||||
ROUTER.register(r'satellites', views.SatelliteViewSet)
|
||||
ROUTER.register(r'transmitters', views.TransmitterViewSet)
|
||||
ROUTER.register(r'telemetry', views.TelemetryViewSet)
|
||||
ROUTER.register(r'tle', views.LatestTleSetViewSet)
|
||||
|
||||
API_URLPATTERNS = [
|
||||
# Keep combatibility by allowing to get satellite object with NORAD
|
||||
# ID.Adding 'basename' value to use it in custom renderers.
|
||||
path(
|
||||
'satellites/<int:satellite_entry__norad_cat_id>/',
|
||||
views.SatelliteViewSet.as_view({'get': 'retrieve'}, basename='latestsatellite')
|
||||
),
|
||||
path('', include(ROUTER.urls))
|
||||
]
|
||||
|
|
716
db/api/views.py
716
db/api/views.py
|
@ -1,69 +1,627 @@
|
|||
"""SatNOGS DB API django rest framework Views"""
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import F
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.timezone import now
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema, \
|
||||
extend_schema_view
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser
|
||||
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from db.api import filters, pagination, serializers
|
||||
from db.base.models import DemodData, Mode, Satellite, Transmitter
|
||||
from db.base.tasks import update_satellite
|
||||
from rest_framework import mixins, status, viewsets
|
||||
from rest_framework.parsers import FileUploadParser, FormParser
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from db.api.parsers import JSONLDParser
|
||||
from db.api.perms import IsAuthenticatedOrOptions, SafeMethodsWithPermission
|
||||
from db.api.renderers import BrowserableJSONLDRenderer, JSONLDRenderer
|
||||
from db.api.throttling import GetTelemetryAnononymousRateThrottle, GetTelemetryUserRateThrottle, \
|
||||
GetTelemetryViolatorThrottle
|
||||
from db.base.helpers import gridsquare
|
||||
from db.base.models import SATELLITE_STATUS, SERVICE_TYPE, TRANSMITTER_STATUS, TRANSMITTER_TYPE, \
|
||||
Artifact, DemodData, LatestTleSet, Mode, Satellite, SatelliteEntry, SatelliteIdentifier, \
|
||||
Transmitter
|
||||
from db.base.tasks import decode_current_frame, publish_current_frame, update_satellite_name
|
||||
|
||||
ISS_EXAMPLE = OpenApiExample('25544 (ISS)', value=25544)
|
||||
|
||||
|
||||
class ModeView(viewsets.ReadOnlyModelViewSet):
|
||||
@extend_schema_view(
|
||||
retrieve=extend_schema(
|
||||
description='Retrieve a single RF Mode from SatNOGS DB based on its ID',
|
||||
),
|
||||
list=extend_schema(description='Retrieve a complete list of RF Modes from SatNOGS DB', )
|
||||
)
|
||||
class ModeViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
|
||||
"""
|
||||
Read-only view into the transmitter modulation modes (RF Modes) currently tracked
|
||||
in the SatNOGS DB database
|
||||
|
||||
For more details on individual RF mode types please [see our wiki][moderef].
|
||||
|
||||
[moderef]: https://wiki.satnogs.org/Category:RF_Modes
|
||||
"""
|
||||
renderer_classes = [
|
||||
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
||||
]
|
||||
queryset = Mode.objects.all()
|
||||
serializer_class = serializers.ModeSerializer
|
||||
|
||||
|
||||
class SatelliteView(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = Satellite.objects.all()
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
description='Retrieve a full or filtered list of satellites in SatNOGS DB',
|
||||
parameters=[
|
||||
# drf-spectacular does not currently recognize the in_orbit filter as a
|
||||
# bool, forcing it here. See drf-spectacular#234
|
||||
OpenApiParameter(
|
||||
name='in_orbit',
|
||||
description='Filter by satellites currently in orbit (True) or those that have \
|
||||
decayed (False)',
|
||||
required=False,
|
||||
type=bool
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='status',
|
||||
description='Filter by satellite status: ' + ' '.join(SATELLITE_STATUS),
|
||||
required=False,
|
||||
type=OpenApiTypes.STR
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='norad_cat_id',
|
||||
description='Select a satellite by its NORAD-assigned identifier',
|
||||
examples=[ISS_EXAMPLE],
|
||||
),
|
||||
],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
description='Retrieve details on a single satellite in SatNOGS DB',
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
'satellite_identifier__sat_id',
|
||||
OpenApiTypes.STR,
|
||||
OpenApiParameter.PATH,
|
||||
description='Select a satellite by its Satellite Identifier',
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
class SatelliteViewSet( # pylint: disable=R0901
|
||||
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
View into the Satellite entities in the SatNOGS DB database
|
||||
"""
|
||||
renderer_classes = [
|
||||
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
||||
]
|
||||
parser_classes = [JSONLDParser]
|
||||
queryset = Satellite.objects.filter(
|
||||
associated_satellite__isnull=True, satellite_entry__approved=True
|
||||
).prefetch_related('associated_with', 'telemetries')
|
||||
serializer_class = serializers.SatelliteSerializer
|
||||
filter_class = filters.SatelliteViewFilter
|
||||
lookup_field = 'norad_cat_id'
|
||||
filterset_class = filters.SatelliteViewFilter
|
||||
lookup_field = 'satellite_identifier__sat_id'
|
||||
|
||||
def get_object(self):
|
||||
queryset = self.get_queryset()
|
||||
# Apply any filter backends
|
||||
queryset = self.filter_queryset(queryset)
|
||||
|
||||
# In case user uses NORAD ID for getting satellite
|
||||
if 'satellite_entry__norad_cat_id' in self.kwargs:
|
||||
norad_cat_id = self.kwargs['satellite_entry__norad_cat_id']
|
||||
return get_object_or_404(queryset, satellite_entry__norad_cat_id=norad_cat_id)
|
||||
|
||||
# Getting satellite by using Satellite Identifier
|
||||
sat_id = self.kwargs['satellite_identifier__sat_id']
|
||||
try:
|
||||
return queryset.get(satellite_identifier__sat_id=sat_id)
|
||||
except Satellite.DoesNotExist:
|
||||
return get_object_or_404(
|
||||
queryset, associated_with__satellite_identifier__sat_id=sat_id
|
||||
)
|
||||
|
||||
def create(self, request, *args, **kwargs): # noqa: C901; pylint: disable=R0911,R0912,R0915
|
||||
"""
|
||||
Creates a satellite suggestion.
|
||||
"""
|
||||
satellites_data = []
|
||||
for satellite_entry in request.data['@graph']:
|
||||
if 'satellite' not in satellite_entry:
|
||||
data = 'Satellite Entry without "satellite" key'
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not satellite_entry['satellite']:
|
||||
data = 'One or more of the required fields are missing.\n Required fields: \
|
||||
name, status, citation'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
satellite = satellite_entry['satellite']
|
||||
create_satellite_identifier = False
|
||||
|
||||
satellite_data = {}
|
||||
if "@id" not in satellite:
|
||||
data = 'Missing "@id" for one or more entries'
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if 'sat_id' in satellite:
|
||||
if isinstance(satellite['sat_id'], list):
|
||||
data = 'Multiple values for "http://schema.org/identifier" or multiple \
|
||||
entries with the same "@id" and different \
|
||||
"http://schema.org/identifier" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
sat_id = satellite['sat_id']
|
||||
try:
|
||||
satellite_object = Satellite.objects.get(satellite_identifier__sat_id=sat_id)
|
||||
satellite_data['satellite_identifier'
|
||||
] = satellite_object.satellite_entry.satellite_identifier.pk
|
||||
satellite_data['norad_follow_id'
|
||||
] = satellite_object.satellite_entry.norad_follow_id
|
||||
satellite_data['description'] = satellite_object.satellite_entry.description
|
||||
satellite_data['dashboard_url'
|
||||
] = satellite_object.satellite_entry.dashboard_url
|
||||
satellite_data['image'] = satellite_object.satellite_entry.image
|
||||
satellite_data['decayed'] = satellite_object.satellite_entry.decayed
|
||||
satellite_data['countries'] = satellite_object.satellite_entry.countries
|
||||
satellite_data['website'] = satellite_object.satellite_entry.website
|
||||
satellite_data['launched'] = satellite_object.satellite_entry.launched
|
||||
satellite_data['deployed'] = satellite_object.satellite_entry.deployed
|
||||
satellite_data['operator'] = satellite_object.satellite_entry.operator
|
||||
except Satellite.DoesNotExist:
|
||||
try:
|
||||
satellite_identifier = SatelliteIdentifier.objects.get(sat_id=sat_id)
|
||||
satellite_data['satellite_identifier'] = satellite_identifier.pk
|
||||
except SatelliteIdentifier.DoesNotExist:
|
||||
satellite_identifier = SatelliteIdentifier.objects.create(sat_id=sat_id)
|
||||
satellite_data['satellite_identifier'] = satellite_identifier.pk
|
||||
|
||||
else:
|
||||
create_satellite_identifier = True
|
||||
|
||||
if isinstance(satellite['status'], list):
|
||||
data = 'Multiple values for "https://schema.space/metasat/status" or multiple \
|
||||
entries with the same "@id" and different \
|
||||
"https://schema.space/metasat/status" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if isinstance(satellite['name'], list):
|
||||
data = 'Multiple values for "https://schema.space/metasat/name" or multiple \
|
||||
entries with the same "@id" and different \
|
||||
"https://schema.space/metasat/name" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if isinstance(satellite['citation'], list):
|
||||
data = 'Multiple values for "https://schema.org/citation" or multiple \
|
||||
entries with the same "@id" and different \
|
||||
"https://schema.org/citation" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
satellite_data['name'] = satellite['name']
|
||||
satellite_data['status'] = satellite['status']
|
||||
satellite_data['citation'] = satellite['citation']
|
||||
satellite_data['created_by'] = request.user.pk
|
||||
|
||||
if 'norad_cat_id' in satellite:
|
||||
if isinstance(satellite['norad_cat_id'], list):
|
||||
data = 'Multiple values for "https://schema.space/metasat/noradID" or \
|
||||
multiple entries with the same "@id" and different \
|
||||
"https://schema.space/metasat/noradId" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
satellite_data['norad_cat_id'] = satellite['norad_cat_id']
|
||||
|
||||
if 'names' in satellite:
|
||||
if isinstance(satellite['norad_cat_id'], list):
|
||||
satellite_data['names'] = '\r\n'.join(satellite['names'])
|
||||
else:
|
||||
satellite_data['names'] = satellite['names']
|
||||
|
||||
if create_satellite_identifier:
|
||||
satellite_identifier = SatelliteIdentifier.objects.create()
|
||||
satellite_data['satellite_identifier'] = satellite_identifier.pk
|
||||
|
||||
satellites_data.append(satellite_data)
|
||||
|
||||
serializer = serializers.SatelliteEntrySerializer(
|
||||
data=satellites_data, many=True, allow_empty=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class TransmitterView(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = Transmitter.objects.all()
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name='satellite__norad_cat_id',
|
||||
description='NORAD ID of a satellite to filter telemetry data for',
|
||||
examples=[ISS_EXAMPLE],
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='status',
|
||||
description='Filter by transmitter status: ' + ' '.join(TRANSMITTER_STATUS),
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
examples=[OpenApiExample('active', value='\'active\'')]
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='service',
|
||||
description='Filter by transmitter service: ' + ' '.join(SERVICE_TYPE),
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
examples=[OpenApiExample('Amateur', value='\'Amateur\'')]
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='type',
|
||||
description='Filter by transmitter type: ' + ' '.join(TRANSMITTER_TYPE),
|
||||
required=False,
|
||||
type=OpenApiTypes.STR,
|
||||
examples=[OpenApiExample('Transmitter', value='\'Transmitter\'')]
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
class TransmitterViewSet( # pylint: disable=R0901
|
||||
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
View into the Transmitter entities in the SatNOGS DB database.
|
||||
Transmitters are inclusive of Transceivers and Transponders
|
||||
"""
|
||||
renderer_classes = [
|
||||
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
||||
]
|
||||
parser_classes = [JSONLDParser]
|
||||
queryset = Transmitter.objects.filter(
|
||||
satellite__satellite_entry__approved=True, satellite__associated_satellite__isnull=True
|
||||
).exclude(status='invalid')
|
||||
serializer_class = serializers.TransmitterSerializer
|
||||
filter_class = filters.TransmitterViewFilter
|
||||
filterset_class = filters.TransmitterViewFilter
|
||||
lookup_field = 'uuid'
|
||||
|
||||
def create(self, request, *args, **kwargs): # noqa: C901; pylint: disable=R0911,R0912,R0915
|
||||
"""
|
||||
Creates a transmitter suggestion.
|
||||
"""
|
||||
transmitters_data = []
|
||||
for transmitter_entry in request.data['@graph']:
|
||||
if 'transmitter' not in transmitter_entry:
|
||||
data = 'Transmitter Entry without "transmitter" key'
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if not transmitter_entry['transmitter']:
|
||||
data = 'One or more of the required fields are missing.\n Required fields: \
|
||||
description, status, citation, service, satellite'
|
||||
|
||||
class TelemetryView(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
queryset = DemodData.objects.all()
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
transmitter = transmitter_entry['transmitter']
|
||||
|
||||
transmitter_data = {}
|
||||
if "@id" not in transmitter:
|
||||
data = 'Missing "@id" for one or more entries'
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if 'uuid' in transmitter:
|
||||
if isinstance(transmitter['uuid'], list):
|
||||
data = 'Multiple values for "http://schema.org/identifier" or multiple \
|
||||
entries with the same "@id" and different \
|
||||
"http://schema.org/identifier" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
transmitter_uuid = transmitter['uuid']
|
||||
if Transmitter.objects.filter(uuid=transmitter_uuid).exists():
|
||||
transmitter_data['uuid'] = transmitter_uuid
|
||||
|
||||
transmitter_data['description'] = transmitter['description']
|
||||
transmitter_data['status'] = transmitter['status']
|
||||
transmitter_data['citation'] = transmitter['citation']
|
||||
transmitter_data['service'] = transmitter['service']
|
||||
transmitter_data['created_by'] = request.user.pk
|
||||
|
||||
try:
|
||||
if transmitter['satellite']:
|
||||
if isinstance(transmitter['satellite'], list):
|
||||
data = 'Multiple values for "https://schema.space/metasat/satellite" \
|
||||
or multiple entries with the same "@id" and different \
|
||||
"https://schema.space/metasat/satellite" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
transmitter_data['satellite'] = Satellite.objects.get(
|
||||
satellite_entry__norad_cat_id=transmitter['satellite']['norad_cat_id']
|
||||
).pk
|
||||
else:
|
||||
data = 'Missing NORAD ID value for Satellite'
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Satellite.DoesNotExist:
|
||||
data = 'Unknown NORAD ID: {}'.format(transmitter['satellite']['norad_cat_id'])
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if 'baud' in transmitter:
|
||||
transmitter_data['baud'] = transmitter['baud']
|
||||
|
||||
if 'invert' in transmitter:
|
||||
transmitter_data['invert'] = transmitter['invert']
|
||||
|
||||
if 'uplink' not in transmitter and 'downlink' not in transmitter:
|
||||
data = 'Missing "https://schema.space/metasat/uplink" or \
|
||||
"https://schema.space/metasat/downlink"'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if 'uplink' in transmitter:
|
||||
if isinstance(transmitter['uplink'], list):
|
||||
data = 'Multiple values for "https://schema.space/metasat/uplink" or multiple \
|
||||
entries with the same "@id" and different \
|
||||
"https://schema.space/metasat/uplink" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'frequency' not in transmitter['uplink']:
|
||||
data = 'Missing "https://schema.space/metasat/frequency" from \
|
||||
"https://schema.space/metasat/uplink" value'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if isinstance(transmitter['uplink']['frequency'], int):
|
||||
transmitter_data['type'] = 'Transceiver'
|
||||
transmitter_data['uplink_low'] = transmitter['uplink']['frequency']
|
||||
else:
|
||||
transmitter_data['type'] = 'Transponder'
|
||||
if 'minimum' not in transmitter['uplink'][
|
||||
'frequency'] or 'maximum' not in transmitter['uplink']['frequency']:
|
||||
data = 'Missing "https://schema.org/minimum" or \
|
||||
"https://schema.org/maximum" from \
|
||||
"https://schema.space/metasat/frequency" value of \
|
||||
"https://schema.space/metasat/uplink"'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
transmitter_data['uplink_low'] = transmitter['uplink']['frequency']['minimum']
|
||||
transmitter_data['uplink_high'] = transmitter['uplink']['frequency']['maximum']
|
||||
if 'mode' in transmitter['uplink']:
|
||||
try:
|
||||
transmitter_data['uplink_mode'] = Mode.objects.get(
|
||||
name=transmitter['uplink']['mode']
|
||||
).pk
|
||||
except Mode.DoesNotExist:
|
||||
data = 'Unknown Mode: {}'.format(transmitter['uplink']['mode'])
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'drift' in transmitter['uplink']:
|
||||
transmitter_data['uplink_drift'] = transmitter['uplink']['drift']
|
||||
else:
|
||||
transmitter_data['type'] = 'Transmitter'
|
||||
|
||||
if 'downlink' in transmitter:
|
||||
if isinstance(transmitter['downlink'], list):
|
||||
data = 'Multiple values for "https://schema.space/metasat/downlink" or \
|
||||
multiple entries with the same "@id" and different \
|
||||
"https://schema.space/metasat/downlink" values'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'frequency' not in transmitter['downlink']:
|
||||
data = 'Missing "https://schema.space/metasat/frequency" from \
|
||||
"https://schema.space/metasat/downlink" value'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if isinstance(transmitter['downlink']['frequency'],
|
||||
int) and not transmitter_data['type'] == 'Transponder':
|
||||
transmitter_data['downlink_low'] = transmitter['downlink']['frequency']
|
||||
elif transmitter_data['type'] == 'Transponder':
|
||||
if 'minimum' not in transmitter['downlink'][
|
||||
'frequency'] or 'maximum' not in transmitter['downlink']['frequency']:
|
||||
data = 'Missing "https://schema.org/minimum" or \
|
||||
"https://schema.org/maximum" from \
|
||||
"https://schema.space/metasat/frequency" value of \
|
||||
"https://schema.space/metasat/downlink"'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
transmitter_data['downlink_low'] = transmitter['downlink']['frequency'][
|
||||
'minimum']
|
||||
transmitter_data['downlink_high'] = transmitter['downlink']['frequency'][
|
||||
'maximum']
|
||||
else:
|
||||
data = 'Expected integer for "https://schema.space/metasat/frequency" value \
|
||||
of "https://schema.space/metasat/downlink"'
|
||||
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'mode' in transmitter['downlink']:
|
||||
try:
|
||||
transmitter_data['downlink_mode'] = Mode.objects.get(
|
||||
name=transmitter['downlink']['mode']
|
||||
).pk
|
||||
except Mode.DoesNotExist:
|
||||
data = 'Unknown Mode: {}'.format(transmitter['downlink']['mode'])
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
if 'drift' in transmitter['downlink']:
|
||||
transmitter_data['downlink_drift'] = transmitter['downlink']['drift']
|
||||
transmitters_data.append(transmitter_data)
|
||||
|
||||
serializer = serializers.TransmitterEntrySerializer(
|
||||
data=transmitters_data, many=True, allow_empty=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
else:
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class LatestTleSetViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
|
||||
"""
|
||||
Read-only view into the most recent two-line elements (TLE) in the SatNOGS DB
|
||||
database
|
||||
"""
|
||||
renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
|
||||
queryset = LatestTleSet.objects.all().select_related('satellite').exclude(
|
||||
latest_distributable__isnull=True
|
||||
).annotate(
|
||||
tle0=F('latest_distributable__tle0'),
|
||||
tle1=F('latest_distributable__tle1'),
|
||||
tle2=F('latest_distributable__tle2'),
|
||||
tle_source=F('latest_distributable__tle_source'),
|
||||
updated=F('latest_distributable__updated')
|
||||
)
|
||||
serializer_class = serializers.LatestTleSetSerializer
|
||||
filterset_class = filters.LatestTleSetViewFilter
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Returns latest TLE queryset depending on user permissions
|
||||
"""
|
||||
if self.request.user.has_perm('base.access_all_tles'):
|
||||
return LatestTleSet.objects.all().select_related('satellite').exclude(
|
||||
latest__isnull=True
|
||||
).annotate(
|
||||
tle0=F('latest__tle0'),
|
||||
tle1=F('latest__tle1'),
|
||||
tle2=F('latest__tle2'),
|
||||
tle_source=F('latest__tle_source'),
|
||||
updated=F('latest__updated')
|
||||
)
|
||||
return self.queryset
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name='app_source',
|
||||
description='The submission source for the telemetry frames: manual (a manual \
|
||||
upload/entry), network (SatNOGS Network observations), or sids \
|
||||
(legacy API submission)',
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='observer',
|
||||
description='(string) name of the observer (submitter) to retrieve telemetry data \
|
||||
from'
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='satellite',
|
||||
description='NORAD ID of a satellite to filter telemetry data for',
|
||||
examples=[ISS_EXAMPLE],
|
||||
),
|
||||
OpenApiParameter(name='transmitter', description='Not currently in use'),
|
||||
],
|
||||
),
|
||||
)
|
||||
class TelemetryViewSet( # pylint: disable=R0901,R0912,R0915
|
||||
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
View into the Telemetry objects in the SatNOGS DB database. Currently,
|
||||
this table is inclusive of all data collected from satellite downlink
|
||||
observations
|
||||
"""
|
||||
renderer_classes = [
|
||||
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
|
||||
]
|
||||
queryset = DemodData.objects.all().select_related('satellite', 'transmitter')
|
||||
serializer_class = serializers.TelemetrySerializer
|
||||
filter_class = filters.TelemetryViewFilter
|
||||
permission_classes = (AllowAny, )
|
||||
parser_classes = (FormParser, FileUploadParser)
|
||||
filterset_class = filters.TelemetryViewFilter
|
||||
permission_classes = [SafeMethodsWithPermission]
|
||||
throttle_classes = [
|
||||
GetTelemetryAnononymousRateThrottle, GetTelemetryUserRateThrottle,
|
||||
GetTelemetryViolatorThrottle
|
||||
]
|
||||
parser_classes = (FormParser, MultiPartParser, FileUploadParser)
|
||||
pagination_class = pagination.LinkedHeaderPageNumberPagination
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
Lists data from satellite if they are filtered by NORAD ID or Satellite ID. Also logs the
|
||||
requests if it is set to do so.
|
||||
"""
|
||||
satellite = request.query_params.get('satellite', None)
|
||||
sat_id = request.query_params.get('sat_id', None)
|
||||
|
||||
if not (satellite or sat_id):
|
||||
data = {
|
||||
'detail': (
|
||||
'For getting data please use either satellite(NORAD ID) filter or'
|
||||
'sat_id(Satellite ID) filter'
|
||||
),
|
||||
'results': None
|
||||
}
|
||||
response = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
response.exception = True
|
||||
return response
|
||||
|
||||
if settings.LOG_TELEMETRY_REQUESTS:
|
||||
user_id = str(request.user.id)
|
||||
remote_address = str(request.META.get("REMOTE_ADDR"))
|
||||
x_forwarded_for = str(request.META.get("HTTP_X_FORWARDED_FOR"))
|
||||
timestamp = now().isoformat()
|
||||
request_data = user_id + ';' + remote_address + ';' + x_forwarded_for + ';' + timestamp
|
||||
cache.set(
|
||||
'telemetry_log_' + user_id + '_' + timestamp, request_data,
|
||||
settings.TELEMETRY_LOGS_TIME_TO_LIVE
|
||||
)
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
responses={'201': None}, # None
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Creates a frame of telemetry data from a satellite observation.
|
||||
"""
|
||||
# pylint: disable=R0914
|
||||
data = {}
|
||||
|
||||
norad_cat_id = request.data.get('noradID')
|
||||
norad_id = request.data.get('noradID')
|
||||
|
||||
if not Satellite.objects.filter(norad_cat_id=norad_cat_id).exists():
|
||||
try:
|
||||
update_satellite(norad_cat_id, update_name=True, update_tle=True)
|
||||
except LookupError:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
try:
|
||||
if norad_id:
|
||||
satellite = Satellite.objects.get(satellite_entry__norad_cat_id=norad_id)
|
||||
else:
|
||||
raise ValueError
|
||||
except Satellite.DoesNotExist:
|
||||
satellite_identifier = SatelliteIdentifier.objects.create()
|
||||
satellite_entry = SatelliteEntry.objects.create(
|
||||
norad_cat_id=norad_id,
|
||||
name='New Satellite',
|
||||
satellite_identifier=satellite_identifier,
|
||||
created=now()
|
||||
)
|
||||
satellite = Satellite.objects.create(
|
||||
satellite_identifier=satellite_identifier, satellite_entry=satellite_entry
|
||||
)
|
||||
update_satellite_name.delay(int(norad_id))
|
||||
except ValueError:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data['satellite'] = Satellite.objects.get(norad_cat_id=norad_cat_id).id
|
||||
data['satellite'] = satellite.id
|
||||
data['station'] = request.data.get('source')
|
||||
timestamp = request.data.get('timestamp')
|
||||
data['timestamp'] = timestamp
|
||||
data['timestamp'] = request.data.get('timestamp')
|
||||
if request.data.get('version'):
|
||||
data['version'] = request.data.get('version')
|
||||
observation_id = ''
|
||||
if request.data.get('observation_id'):
|
||||
observation_id = request.data.get('observation_id')
|
||||
data['observation_id'] = observation_id
|
||||
station_id = ''
|
||||
if request.data.get('station_id'):
|
||||
station_id = request.data.get('station_id')
|
||||
data['station_id'] = station_id
|
||||
|
||||
# Convert coordinates to omit N-S and W-E designators
|
||||
lat = request.data.get('latitude')
|
||||
lng = request.data.get('longitude')
|
||||
if any(x.isalpha() for x in lat):
|
||||
data['lat'] = (-float(lat[:-1]) if ('S' in lat) else float(lat[:-1]))
|
||||
else:
|
||||
data['lat'] = float(lat)
|
||||
if any(x.isalpha() for x in lng):
|
||||
data['lng'] = (-float(lng[:-1]) if ('W' in lng) else float(lng[:-1]))
|
||||
else:
|
||||
data['lng'] = float(lng)
|
||||
try:
|
||||
if any(x.isalpha() for x in lat):
|
||||
data['lat'] = (-float(lat[:-1]) if ('S' in lat) else float(lat[:-1]))
|
||||
else:
|
||||
data['lat'] = float(lat)
|
||||
if any(x.isalpha() for x in lng):
|
||||
data['lng'] = (-float(lng[:-1]) if ('W' in lng) else float(lng[:-1]))
|
||||
else:
|
||||
data['lng'] = float(lng)
|
||||
except ValueError:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Network or SiDS submission?
|
||||
if request.data.get('satnogs_network'):
|
||||
|
@ -74,9 +632,89 @@ class TelemetryView(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.Cre
|
|||
# Create file out of frame string
|
||||
frame = ContentFile(request.data.get('frame'), name='sids')
|
||||
data['payload_frame'] = frame
|
||||
# Create observer
|
||||
qth = gridsquare(data['lat'], data['lng'])
|
||||
observer = '{0}-{1}'.format(data['station'], qth)
|
||||
data['observer'] = observer
|
||||
|
||||
serializer = serializers.SidsSerializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
# Run task to decode the current frame
|
||||
decode_current_frame.delay(satellite.satellite_identifier.sat_id, serializer.instance.pk)
|
||||
|
||||
# Run task to publish the current frame via ZeroMQ
|
||||
if settings.ZEROMQ_ENABLE:
|
||||
publish_current_frame.delay(
|
||||
request.data.get('timestamp'), request.data.get('frame'), observer, {
|
||||
'norad_id': norad_id,
|
||||
'observation_id': observation_id,
|
||||
'station_id': station_id
|
||||
}
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
'network_obs_id',
|
||||
OpenApiTypes.INT64,
|
||||
required=False,
|
||||
description='Given a SatNOGS Network observation ID, this will return any \
|
||||
artifacts files associated with the observation.'
|
||||
),
|
||||
],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
'id',
|
||||
OpenApiTypes.URI,
|
||||
OpenApiParameter.PATH,
|
||||
description='The ID for the requested artifact entry in DB'
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
class ArtifactViewSet( # pylint: disable=R0901
|
||||
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""
|
||||
Artifacts are file-formatted objects collected from a satellite observation.
|
||||
"""
|
||||
queryset = Artifact.objects.all()
|
||||
filterset_class = filters.ArtifactViewFilter
|
||||
permission_classes = [IsAuthenticatedOrOptions]
|
||||
parser_classes = (FormParser, MultiPartParser)
|
||||
pagination_class = pagination.LinkedHeaderPageNumberPagination
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Returns the right serializer depending on http method that is used"""
|
||||
if self.action == 'create':
|
||||
return serializers.NewArtifactSerializer
|
||||
return serializers.ArtifactSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Creates observation artifact from an [HDF5 formatted file][hdf5ref]
|
||||
* Requires session or key authentication to create an artifact
|
||||
|
||||
[hdf5ref]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format
|
||||
"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
try:
|
||||
if serializer.is_valid():
|
||||
data = serializer.save()
|
||||
http_response = {}
|
||||
http_response['id'] = data.id
|
||||
response = Response(http_response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
data = serializer.errors
|
||||
response = Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
except (ValidationError, ValueError, OSError) as error:
|
||||
response = Response(str(error), status=status.HTTP_400_BAD_REQUEST)
|
||||
return response
|
||||
|
|
528
db/base/admin.py
528
db/base/admin.py
|
@ -1,133 +1,402 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
"""Defines functions and settings for the django admin interface"""
|
||||
from socket import error as socket_error
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin, messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.urls import re_path, reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from db.base.models import DemodData, Mode, Satellite, Telemetry, \
|
||||
from db.base.models import Artifact, DemodData, ExportedFrameset, LatestTleSet, Mode, Operator, \
|
||||
Satellite, SatelliteEntry, SatelliteIdentifier, SatelliteSuggestion, Telemetry, Tle, \
|
||||
Transmitter, TransmitterEntry, TransmitterSuggestion
|
||||
from db.base.tasks import check_celery, decode_all_data, reset_decoded_data
|
||||
|
||||
logger = logging.getLogger('db')
|
||||
from db.base.tasks import check_celery, decode_all_data, update_tle_sets
|
||||
from db.base.utils import update_latest_tle_sets
|
||||
|
||||
|
||||
@admin.register(Mode)
|
||||
class ModeAdmin(admin.ModelAdmin):
|
||||
"""Defines Mode view in django admin UI"""
|
||||
list_display = ('name', )
|
||||
|
||||
|
||||
@admin.register(Operator)
|
||||
class OperatorAdmin(admin.ModelAdmin):
|
||||
"""Defines Operator view in django admin UI"""
|
||||
list_display = ('name', 'names', 'website')
|
||||
search_fields = ('name', 'names')
|
||||
|
||||
|
||||
@admin.register(SatelliteIdentifier)
|
||||
class SatelliteIdentifierAdmin(admin.ModelAdmin):
|
||||
"""Defines SatelliteIdentifier view in django admin UI"""
|
||||
list_display = ('id', 'sat_id', 'created')
|
||||
search_fields = ('sat_id', )
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(SatelliteEntry)
|
||||
class SatelliteEntryAdmin(admin.ModelAdmin):
|
||||
"""Defines Satellite Entry view in django admin UI"""
|
||||
list_display = (
|
||||
'id', 'satellite_identifier', 'name', 'norad_cat_id', 'status', 'decayed',
|
||||
'norad_follow_id', 'citation', 'approved', 'created', 'created_by', 'reviewed', 'reviewer'
|
||||
)
|
||||
search_fields = ('name', 'norad_cat_id', 'norad_follow_id', 'satellite_identifier__sat_id')
|
||||
list_filter = ('status', 'decayed', 'reviewed', 'approved', 'satellite_identifier__sat_id')
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('satellite_identifier')
|
||||
|
||||
# workaround for readonly CountryField, more at:
|
||||
# https://github.com/SmileyChris/django-countries/issues/298
|
||||
def get_fields(self, request, obj=None):
|
||||
fields = super().get_fields(request, obj)
|
||||
if not self.has_change_permission(request):
|
||||
try:
|
||||
index = fields.index('countries')
|
||||
fields[index] = 'countries_str'
|
||||
except ValueError:
|
||||
pass
|
||||
return fields
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
if request.user.has_perm('base.delete_satelliteentry'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(SatelliteSuggestion)
|
||||
class SatelliteSuggestionAdmin(admin.ModelAdmin):
|
||||
"""Defines SatelliteSuggestion view in django admin UI"""
|
||||
list_display = (
|
||||
'id', 'satellite_identifier', 'name', 'norad_cat_id', 'citation', 'created', 'created_by'
|
||||
)
|
||||
search_fields = ('name', 'norad_cat_id', 'norad_follow_id', 'satellite_identifier__sat_id')
|
||||
list_filter = ('satellite_identifier', )
|
||||
actions = ['approve_suggestion', 'reject_suggestion']
|
||||
|
||||
# workaround for readonly CountryField, more at:
|
||||
# https://github.com/SmileyChris/django-countries/issues/298
|
||||
def get_fields(self, request, obj=None):
|
||||
fields = super().get_fields(request, obj)
|
||||
if not self.has_change_permission(request):
|
||||
try:
|
||||
index = fields.index('countries')
|
||||
fields[index] = 'countries_str'
|
||||
except ValueError:
|
||||
pass
|
||||
return fields
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
"""Returns the actions a user can take on a SatelliteSuggestion
|
||||
|
||||
For example, delete, approve, or reject
|
||||
|
||||
:returns: list of actions the user can take on SatelliteSuggestion
|
||||
"""
|
||||
actions = super().get_actions(request)
|
||||
if not request.user.has_perm('base.delete_satellitesuggestion'):
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
def approve_suggestion(self, request, queryset):
|
||||
"""Returns the SatelliteSuggestion page after approving suggestions
|
||||
|
||||
:param queryset: the SatelliteSuggestion entries to be approved
|
||||
:returns: SatelliteSuggestion admin page
|
||||
"""
|
||||
queryset_size = len(queryset)
|
||||
for entry in queryset:
|
||||
satellite = Satellite.objects.get(satellite_identifier=entry.satellite_identifier)
|
||||
entry.approved = True
|
||||
entry.reviewed = now()
|
||||
entry.reviewer = request.user
|
||||
entry.save()
|
||||
satellite.satellite_entry = entry
|
||||
satellite.save()
|
||||
if queryset_size == 1:
|
||||
self.message_user(request, "Satellite suggestion was successfully approved")
|
||||
else:
|
||||
self.message_user(request, "Satellite suggestions were successfully approved")
|
||||
|
||||
approve_suggestion.short_description = 'Approve selected satellite suggestions'
|
||||
|
||||
def reject_suggestion(self, request, queryset):
|
||||
"""Returns the SatelliteSuggestion page after rejecting suggestions
|
||||
|
||||
:param queryset: the SatelliteSuggestion entries to be rejected
|
||||
:returns: SatelliteSuggestion admin page
|
||||
"""
|
||||
queryset_size = len(queryset)
|
||||
for entry in queryset:
|
||||
entry.approved = False
|
||||
entry.reviewed = now()
|
||||
entry.reviewer = request.user
|
||||
entry.save()
|
||||
if queryset_size == 1:
|
||||
self.message_user(request, "Satellite suggestion was successfully rejected")
|
||||
else:
|
||||
self.message_user(request, "Satellite suggestions were successfully rejected")
|
||||
|
||||
reject_suggestion.short_description = 'Reject selected satellite suggestions'
|
||||
|
||||
|
||||
@admin.register(Satellite)
|
||||
class SatelliteAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'norad_cat_id', 'status', 'decayed')
|
||||
search_fields = ('name', 'norad_cat_id')
|
||||
list_filter = ('status', 'decayed')
|
||||
"""Defines Satellite view in django admin UI"""
|
||||
list_display = (
|
||||
'id', 'sat_id', 'associated_satellite', 'last_modified', 'satellite_entry_pk',
|
||||
'norad_cat_id', 'name', 'norad_follow_id', 'status', 'decayed'
|
||||
)
|
||||
search_fields = (
|
||||
'satellite_identifier__sat_id', 'satellite_entry__name', 'satellite_entry__norad_cat_id',
|
||||
'satellite_entry__norad_follow_id'
|
||||
)
|
||||
list_filter = ('satellite_entry__status', 'satellite_entry__decayed', 'associated_satellite')
|
||||
|
||||
def get_urls(self):
|
||||
urls = super(SatelliteAdmin, self).get_urls()
|
||||
"""Returns django urls for the Satellite view
|
||||
|
||||
check_celery -- url for the check_celery function
|
||||
decode_all_data -- url for the decode_all_data function
|
||||
|
||||
:returns: Django urls for the Satellite admin view
|
||||
"""
|
||||
urls = super().get_urls()
|
||||
my_urls = [
|
||||
url(r'^check_celery/$', self.check_celery, name='check_celery'),
|
||||
url(r'^reset_data/(?P<norad>[0-9]+)/$', self.reset_data, name='reset_data'),
|
||||
url(
|
||||
r'^decode_all_data/(?P<norad>[0-9]+)/$',
|
||||
re_path(r'^check_celery/$', self.check_celery, name='check_celery'),
|
||||
re_path(
|
||||
r'^decode_all_data/(?P<sat_id>[A-Z]{4,4}(?:-\d\d\d\d){4,4})/$',
|
||||
self.decode_all_data,
|
||||
name='decode_all_data'
|
||||
),
|
||||
)
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
def check_celery(self, request):
|
||||
def sat_id(self, obj): # pylint: disable=R0201
|
||||
"""Return the Satellite Identifier for that satellite"""
|
||||
return obj.satellite_identifier.sat_id
|
||||
|
||||
def satellite_entry_pk(self, obj): # pylint: disable=R0201
|
||||
"""Return the pk of the Satellite Entry object for that satellite"""
|
||||
if obj.satellite_entry:
|
||||
return obj.satellite_entry.pk
|
||||
return None
|
||||
|
||||
def norad_cat_id(self, obj): # pylint: disable=R0201
|
||||
"""Return the satellite NORAD ID"""
|
||||
if obj.satellite_entry:
|
||||
return obj.satellite_entry.norad_cat_id
|
||||
return None
|
||||
|
||||
def norad_follow_id(self, obj): # pylint: disable=R0201
|
||||
"""Return the NORAD ID that satellite follows"""
|
||||
if obj.satellite_entry:
|
||||
return obj.satellite_entry.norad_follow_id
|
||||
return None
|
||||
|
||||
def name(self, obj): # pylint: disable=R0201
|
||||
"""Return the satellite name"""
|
||||
if obj.satellite_entry:
|
||||
return obj.satellite_entry.name
|
||||
return None
|
||||
|
||||
def status(self, obj): # pylint: disable=R0201
|
||||
"""Return the satellite status"""
|
||||
if obj.satellite_entry:
|
||||
return obj.satellite_entry.status
|
||||
return None
|
||||
|
||||
def decayed(self, obj): # pylint: disable=R0201
|
||||
"""Return the dacayed date of the satellite"""
|
||||
if obj.satellite_entry:
|
||||
return obj.satellite_entry.decayed
|
||||
return None
|
||||
|
||||
def check_celery(self, request): # pylint: disable=R0201
|
||||
"""Returns status of Celery workers
|
||||
|
||||
Check the delay for celery workers, return an error if a connection
|
||||
can not be made or if the delay is too long. Otherwise return that
|
||||
Celery is OK.
|
||||
|
||||
:returns: admin home page redirect with popup message
|
||||
"""
|
||||
try:
|
||||
investigator = check_celery.delay()
|
||||
except socket_error as e:
|
||||
messages.error(request, 'Cannot connect to broker: %s' % e)
|
||||
except socket_error as error:
|
||||
messages.error(request, 'Cannot connect to broker: %s' % error)
|
||||
return HttpResponseRedirect(reverse('admin:index'))
|
||||
|
||||
try:
|
||||
investigator.get(timeout=5)
|
||||
except investigator.TimeoutError as e:
|
||||
messages.error(request, 'Worker timeout: %s' % e)
|
||||
except investigator.TimeoutError as error:
|
||||
messages.error(request, 'Worker timeout: %s' % error)
|
||||
else:
|
||||
messages.success(request, 'Celery is OK')
|
||||
finally:
|
||||
return HttpResponseRedirect(reverse('admin:index'))
|
||||
|
||||
# resets all decoded data and changes the is_decoded flag back to False
|
||||
# THIS IS VERY DISTRUCTIVE, but the expectation is that a decode_all_data
|
||||
# would follow.
|
||||
def reset_data(self, request, norad):
|
||||
reset_decoded_data.delay(norad)
|
||||
messages.success(request, 'Data reset task was triggered successfully!')
|
||||
return redirect(reverse('admin:index'))
|
||||
return HttpResponseRedirect(reverse('admin:index'))
|
||||
|
||||
# force a decode of all data for a norad ID. This could be very resource
|
||||
# intensive but necessary when catching a satellite up with a new decoder
|
||||
def decode_all_data(self, request, norad):
|
||||
decode_all_data.delay(norad)
|
||||
def decode_all_data(self, request, sat_id): # pylint: disable=R0201
|
||||
"""Returns the admin home page, while triggering a Celery decode task
|
||||
|
||||
Forces a decode of all data for a Satellite Identifier. This could be very resource
|
||||
intensive but necessary when catching a satellite up with a new decoder
|
||||
|
||||
:param sat_id: the Satellite Identifier for the satellite to decode
|
||||
:returns: Admin home page
|
||||
"""
|
||||
satellite = Satellite.objects.get(satellite_identifier__sat_id=sat_id)
|
||||
|
||||
# Allow decoding data only for Satellites that are not merged and
|
||||
# suggest user trigger decoding for the associated_satellite which will
|
||||
# include all DemodData of the satellites that are associated with it
|
||||
if satellite.associated_satellite:
|
||||
messages.error(
|
||||
request,
|
||||
'Satellite has been merged, for decoding data trigger "Decode All Data" for "%s"'
|
||||
% satellite.associated_satellite
|
||||
)
|
||||
return redirect(reverse('admin:index'))
|
||||
decode_all_data.delay(sat_id)
|
||||
messages.success(request, 'Decode task was triggered successfully!')
|
||||
return redirect(reverse('admin:index'))
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(TransmitterEntry)
|
||||
class TransmitterEntryAdmin(admin.ModelAdmin):
|
||||
"""Defines TransmitterEntry view in django admin UI"""
|
||||
list_display = (
|
||||
'uuid', 'description', 'satellite', 'type', 'mode', 'baud', 'downlink_low',
|
||||
'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high', 'uplink_drift', 'reviewed',
|
||||
'approved', 'status', 'created', 'citation', 'user'
|
||||
'id', 'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode',
|
||||
'uplink_mode', 'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low',
|
||||
'uplink_high', 'uplink_drift', 'citation', 'approved', 'status', 'created', 'created_by',
|
||||
'reviewed', 'reviewer'
|
||||
)
|
||||
search_fields = (
|
||||
'uuid', 'satellite__satellite_identifier__sat_id', 'satellite__satellite_entry__name',
|
||||
'satellite__satellite_entry__norad_cat_id'
|
||||
)
|
||||
search_fields = ('satellite__id', 'uuid', 'satellite__name', 'satellite__norad_cat_id')
|
||||
list_filter = (
|
||||
'reviewed',
|
||||
'approved',
|
||||
'type',
|
||||
'status',
|
||||
'mode',
|
||||
'service',
|
||||
'downlink_mode',
|
||||
'uplink_mode',
|
||||
'baud',
|
||||
)
|
||||
readonly_fields = ('uuid', 'satellite')
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
if request.user.has_perm('base.delete_transmitterentry'):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(TransmitterSuggestion)
|
||||
class TransmitterSuggestionAdmin(admin.ModelAdmin):
|
||||
"""Defines TransmitterSuggestion view in django admin UI"""
|
||||
list_display = (
|
||||
'uuid', 'description', 'satellite', 'type', 'mode', 'baud', 'downlink_low',
|
||||
'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high', 'uplink_drift', 'status',
|
||||
'created', 'citation', 'user'
|
||||
'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode', 'uplink_mode',
|
||||
'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high',
|
||||
'uplink_drift', 'citation', 'status', 'created', 'created_by'
|
||||
)
|
||||
search_fields = (
|
||||
'uuid', 'satellite__satellite_identifier__sat_id', 'satellite__satellite_entry__name',
|
||||
'satellite__satellite_entry__norad_cat_id'
|
||||
)
|
||||
search_fields = ('satellite__id', 'uuid', 'satellite__name', 'satellite__norad_cat_id')
|
||||
list_filter = (
|
||||
'type',
|
||||
'mode',
|
||||
'downlink_mode',
|
||||
'uplink_mode',
|
||||
'baud',
|
||||
)
|
||||
readonly_fields = (
|
||||
'uuid', 'description', 'status', 'type', 'uplink_low', 'uplink_high', 'uplink_drift',
|
||||
'downlink_low', 'downlink_high', 'downlink_drift', 'mode', 'invert', 'baud', 'satellite',
|
||||
'reviewed', 'approved', 'created', 'citation', 'user'
|
||||
'service',
|
||||
)
|
||||
actions = ['approve_suggestion', 'reject_suggestion']
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super(TransmitterSuggestionAdmin, self).get_actions(request)
|
||||
"""Returns the actions a user can take on a TransmitterSuggestion
|
||||
|
||||
For example, delete, approve, or reject
|
||||
|
||||
:returns: list of actions the user can take on TransmitterSuggestion
|
||||
"""
|
||||
actions = super().get_actions(request)
|
||||
if not request.user.has_perm('base.delete_transmittersuggestion'):
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
def approve_suggestion(self, request, queryset):
|
||||
"""Returns the TransmitterSuggestion page after approving suggestions
|
||||
|
||||
:param queryset: the TransmitterSuggestion entries to be approved
|
||||
:returns: TransmitterSuggestion admin page
|
||||
"""
|
||||
queryset_size = len(queryset)
|
||||
for entry in queryset:
|
||||
entry.approved = True
|
||||
entry.reviewed = True
|
||||
entry.created = datetime.utcnow()
|
||||
entry.user = request.user
|
||||
entry.reviewed = now()
|
||||
entry.reviewer = request.user
|
||||
entry.save()
|
||||
# After creating the new approved entries, we update the suggestion entries as reviewed
|
||||
# Note that queryset.update doesn't use model's save() that creates new entries
|
||||
queryset.update(reviewed=True, approved=True)
|
||||
if queryset_size == 1:
|
||||
self.message_user(request, "Transmitter suggestion was successfully approved")
|
||||
else:
|
||||
|
@ -136,16 +405,17 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
|
|||
approve_suggestion.short_description = 'Approve selected transmitter suggestions'
|
||||
|
||||
def reject_suggestion(self, request, queryset):
|
||||
"""Returns the TransmitterSuggestion page after rejecting suggestions
|
||||
|
||||
:param queryset: the TransmitterSuggestion entries to be rejected
|
||||
:returns: TransmitterSuggestion admin page
|
||||
"""
|
||||
queryset_size = len(queryset)
|
||||
for entry in queryset:
|
||||
entry.created = datetime.utcnow()
|
||||
entry.user = request.user
|
||||
entry.approved = False
|
||||
entry.reviewed = True
|
||||
entry.reviewed = now()
|
||||
entry.reviewer = request.user
|
||||
entry.save()
|
||||
# After creating the new approved entries, we update the suggestion entries as reviewed
|
||||
# Note that queryset.update doesn't use model's save() that creates new entries
|
||||
queryset.update(reviewed=True, approved=False)
|
||||
if queryset_size == 1:
|
||||
self.message_user(request, "Transmitter suggestion was successfully rejected")
|
||||
else:
|
||||
|
@ -156,30 +426,140 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(Transmitter)
|
||||
class TransmitterAdmin(admin.ModelAdmin):
|
||||
"""Defines Transmitter view in django admin UI"""
|
||||
list_display = (
|
||||
'uuid', 'description', 'satellite', 'type', 'mode', 'baud', 'downlink_low',
|
||||
'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high', 'uplink_drift', 'status',
|
||||
'created', 'citation', 'user'
|
||||
'id', 'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode',
|
||||
'uplink_mode', 'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low',
|
||||
'uplink_high', 'uplink_drift', 'citation', 'status', 'created', 'created_by', 'reviewed',
|
||||
'reviewer'
|
||||
)
|
||||
search_fields = (
|
||||
'uuid', 'satellite__satellite_identifier__sat_id', 'satellite__satellite_entry__name',
|
||||
'satellite__satellite_entry__norad_cat_id'
|
||||
)
|
||||
search_fields = ('satellite__id', 'uuid', 'satellite__name', 'satellite__norad_cat_id')
|
||||
list_filter = (
|
||||
'type',
|
||||
'status',
|
||||
'mode',
|
||||
'service',
|
||||
'downlink_mode',
|
||||
'uplink_mode',
|
||||
'baud',
|
||||
)
|
||||
readonly_fields = ('uuid', 'satellite')
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions['delete_selected']
|
||||
return actions
|
||||
|
||||
|
||||
@admin.register(Tle)
|
||||
class TleAdmin(admin.ModelAdmin):
|
||||
"""Define TLE view in django admin UI"""
|
||||
list_display = ('satellite_name', 'tle0', 'tle1', 'updated', 'tle_source')
|
||||
list_filter = ('tle_source', 'satellite__satellite_entry__name')
|
||||
|
||||
def satellite_name(self, obj): # pylint: disable=no-self-use
|
||||
"""Return the satellite name"""
|
||||
return obj.satellite.satellite_entry.name
|
||||
|
||||
def get_urls(self):
|
||||
"""Returns django urls for Tle view
|
||||
|
||||
update_tle_sets -- url for the update_tle_sets function
|
||||
|
||||
:returns: Django urls for the Tle admin view
|
||||
"""
|
||||
urls = super().get_urls()
|
||||
my_urls = [
|
||||
re_path(r'^update_tle_sets/$', self.update_tle_sets, name='update_tle_sets'),
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
def update_tle_sets(self, request): # pylint: disable=R0201
|
||||
"""Returns the admin home page, while triggering a Celery update tle sets task
|
||||
|
||||
:returns: Admin home page
|
||||
"""
|
||||
update_tle_sets.delay()
|
||||
messages.success(request, 'Update TLE sets task was triggered successfully!')
|
||||
return redirect(reverse('admin:index'))
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
super().save_model(request, obj, form, change)
|
||||
update_latest_tle_sets(satellite_pks=[obj.satellite.pk])
|
||||
|
||||
def delete_model(self, request, obj):
|
||||
super().delete_model(request, obj)
|
||||
update_latest_tle_sets(satellite_pks=[obj.satellite.pk])
|
||||
|
||||
def delete_queryset(self, request, queryset):
|
||||
satellites = [tle.satellite.pk for tle in queryset]
|
||||
super().delete_queryset(request, queryset)
|
||||
update_latest_tle_sets(satellite_pks=satellites)
|
||||
|
||||
|
||||
@admin.register(LatestTleSet)
|
||||
class LatestTleSetAdmin(admin.ModelAdmin):
|
||||
"""Defines LatestTleSet view in django admin UI"""
|
||||
list_display = ('satellite', 'latest', 'latest_distributable', 'last_modified')
|
||||
search_fields = (
|
||||
'satellite__satellite_identifier__sat_id', 'satellite__satellite_entry__norad_cat_id',
|
||||
'satellite__satellite_entry__name'
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Telemetry)
|
||||
class TelemetryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'decoder')
|
||||
"""Defines Telemetry view in django admin UI"""
|
||||
list_display = ('name', 'decoder', 'satellite')
|
||||
search_fields = (
|
||||
'satellite__satellite_identifier__sat_id', 'satellite__satellite_entry__norad_cat_id',
|
||||
'satellite__satellite_entry__name'
|
||||
)
|
||||
|
||||
|
||||
@admin.register(DemodData)
|
||||
class DemodDataAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'satellite', 'app_source', 'observer')
|
||||
search_fields = ('transmitter__uuid', 'satellite__norad_cat_id', 'observer')
|
||||
"""Defines DemodData view in django admin UI"""
|
||||
list_display = ('id', 'satellite', 'app_source', 'observer', 'observation_id', 'station_id')
|
||||
search_fields = (
|
||||
'transmitter__uuid', 'satellite__satellite_identifier__sat_id',
|
||||
'satellite__satellite_entry__norad_cat_id', 'observer', 'observation_id', 'station_id'
|
||||
)
|
||||
list_filter = (
|
||||
'satellite',
|
||||
'app_source',
|
||||
'observer',
|
||||
)
|
||||
|
||||
def satellite(self, obj):
|
||||
def satellite(self, obj): # pylint: disable=R0201
|
||||
"""Returns the Satellite object associated with this DemodData
|
||||
|
||||
:param obj: DemodData object
|
||||
:returns: Satellite object
|
||||
"""
|
||||
return obj.satellite
|
||||
|
||||
|
||||
@admin.register(ExportedFrameset)
|
||||
class ExportedFramesetAdmin(admin.ModelAdmin):
|
||||
"""Defines ExportedFrameset view in django admin UI"""
|
||||
list_display = ('id', 'created', 'user', 'satellite', 'exported_file', 'start', 'end')
|
||||
search_fields = ('user', 'satellite__satellite_entry__norad_cat_id')
|
||||
list_filter = ('satellite', 'user')
|
||||
|
||||
|
||||
@admin.register(Artifact)
|
||||
class ArtifactAdmin(admin.ModelAdmin):
|
||||
"""Defines Artifact view in django admin UI"""
|
||||
list_display = ('id', 'network_obs_id', 'artifact_file')
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
"""SatNOGS DB Base app config"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BaseConfig(AppConfig):
|
||||
"""Set configuration of the SatNOGS DB Base app"""
|
||||
name = 'db.base'
|
||||
verbose_name = "Base"
|
||||
|
||||
def ready(self):
|
||||
from db.base import signals # noqa: F401; pylint: disable=C0415,W0611
|
|
@ -1,3 +1,4 @@
|
|||
"""SatNOGS DB django context processors"""
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from satnogsdecoders import __version__ as satnogsdecoders_version
|
||||
|
@ -8,33 +9,46 @@ from db import __version__
|
|||
def analytics(request):
|
||||
"""Returns analytics code."""
|
||||
if settings.ENVIRONMENT == 'production':
|
||||
return {'analytics_code': render_to_string('includes/analytics.html')}
|
||||
rendered_string = {'analytics_code': render_to_string('includes/analytics.html')}
|
||||
else:
|
||||
return {'analytics_code': ''}
|
||||
rendered_string = {'analytics_code': ''}
|
||||
return rendered_string
|
||||
|
||||
|
||||
def stage_notice(request):
|
||||
"""Displays stage notice."""
|
||||
if settings.ENVIRONMENT == 'stage':
|
||||
return {'stage_notice': render_to_string('includes/stage_notice.html')}
|
||||
rendered_string = {'stage_notice': render_to_string('includes/stage_notice.html')}
|
||||
else:
|
||||
return {'stage_notice': ''}
|
||||
rendered_string = {'stage_notice': ''}
|
||||
return rendered_string
|
||||
|
||||
|
||||
def auth_block(request):
|
||||
"""Displays auth links local vs auth0."""
|
||||
if settings.AUTH0:
|
||||
return {'auth_block': render_to_string('includes/auth_auth0.html')}
|
||||
rendered_string = {'auth_block': render_to_string('includes/auth_auth0.html')}
|
||||
else:
|
||||
return {'auth_block': render_to_string('includes/auth_local.html')}
|
||||
rendered_string = {'auth_block': render_to_string('includes/auth_local.html')}
|
||||
return rendered_string
|
||||
|
||||
|
||||
def logout_block(request):
|
||||
"""Displays logout links local vs auth0."""
|
||||
if settings.AUTH0:
|
||||
return {'logout_block': render_to_string('includes/logout_auth0.html')}
|
||||
rendered_string = {'logout_block': render_to_string('includes/logout_auth0.html')}
|
||||
else:
|
||||
return {'logout_block': render_to_string('includes/logout_local.html')}
|
||||
rendered_string = {'logout_block': render_to_string('includes/logout_local.html')}
|
||||
return rendered_string
|
||||
|
||||
|
||||
def login_button(request):
|
||||
"""Displays login button local vs auth0."""
|
||||
if settings.AUTH0:
|
||||
rendered_string = {'login_button': render_to_string('includes/login_button_auth0.html')}
|
||||
else:
|
||||
rendered_string = {'login_button': render_to_string('includes/login_button_local.html')}
|
||||
return rendered_string
|
||||
|
||||
|
||||
def version(request):
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
[
|
||||
{
|
||||
"model": "base.operator",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Libre Space Foundation",
|
||||
"names": "LSF",
|
||||
"description": "The Libre Space Foundation promotes open source space technologies.",
|
||||
"website": "https://libre.space"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "base.operator",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Radio Amateur Satellite Corporation",
|
||||
"names": "AMSAT",
|
||||
"description": "The goal of AMSAT is to foster Amateur Radio’s participation in space research and communication.",
|
||||
"website": "https://www.amsat.org"
|
||||
}
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
159
db/base/forms.py
159
db/base/forms.py
|
@ -1,23 +1,152 @@
|
|||
from django import forms
|
||||
"""SatNOGS DB django base Forms class"""
|
||||
from bootstrap_modal_forms.forms import BSModalForm, BSModalModelForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.forms import ModelChoiceField, TextInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from db.base.models import Transmitter, TransmitterEntry
|
||||
from db.base.models import Satellite, SatelliteEntry, Transmitter, TransmitterEntry
|
||||
|
||||
|
||||
class TransmitterEntryForm(forms.ModelForm):
|
||||
def existing_uuid(value):
|
||||
try:
|
||||
Transmitter.objects.get(uuid=value)
|
||||
except Transmitter.DoesNotExist:
|
||||
raise ValidationError(
|
||||
_('%(value)s is not a valid uuid'),
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
def existing_uuid(value):
|
||||
"""ensures the UUID is existing and valid"""
|
||||
try:
|
||||
Transmitter.objects.get(uuid=value)
|
||||
except Transmitter.DoesNotExist as error:
|
||||
raise ValidationError(
|
||||
_('%(value)s is not a valid uuid'),
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
) from error
|
||||
|
||||
uuid = forms.CharField(required=False, validators=[existing_uuid])
|
||||
|
||||
class TransmitterCreateForm(BSModalModelForm): # pylint: disable=too-many-ancestors
|
||||
"""Model Form class for TransmitterEntry objects"""
|
||||
class Meta:
|
||||
model = TransmitterEntry
|
||||
exclude = ['uuid', 'reviewed', 'approved', 'created', 'user']
|
||||
fields = [
|
||||
'description', 'type', 'status', 'uplink_low', 'uplink_high', 'uplink_drift',
|
||||
'uplink_mode', 'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode',
|
||||
'invert', 'baud', 'citation', 'service', 'iaru_coordination', 'iaru_coordination_url',
|
||||
'itu_notification'
|
||||
]
|
||||
labels = {
|
||||
'downlink_low': _('Downlink freq.'),
|
||||
'uplink_low': _('Uplink freq.'),
|
||||
'invert': _('Inverted Transponder?'),
|
||||
'iaru_coordination': _('IARU Coordination'),
|
||||
'iaru_coordination_url': _('IARU Coordination URL'),
|
||||
'itu_notification': _('ITU Notifications URLs'),
|
||||
}
|
||||
widgets = {
|
||||
'description': TextInput(),
|
||||
}
|
||||
|
||||
|
||||
class TransmitterUpdateForm(BSModalModelForm): # pylint: disable=too-many-ancestors
|
||||
"""Model Form class for TransmitterEntry objects"""
|
||||
class Meta:
|
||||
model = TransmitterEntry
|
||||
fields = [
|
||||
'description', 'type', 'status', 'uplink_low', 'uplink_high', 'uplink_drift',
|
||||
'uplink_mode', 'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode',
|
||||
'invert', 'baud', 'citation', 'service', 'iaru_coordination', 'iaru_coordination_url',
|
||||
'itu_notification'
|
||||
]
|
||||
labels = {
|
||||
'downlink_low': _('Downlink freq.'),
|
||||
'uplink_low': _('Uplink freq.'),
|
||||
'invert': _('Inverted Transponder?'),
|
||||
'iaru_coordination': _('IARU Coordination'),
|
||||
'iaru_coordination_url': _('IARU Coordination URL'),
|
||||
'itu_notification': _('ITU Notifications URLs'),
|
||||
}
|
||||
widgets = {
|
||||
'description': TextInput(),
|
||||
}
|
||||
|
||||
|
||||
class SatelliteCreateForm(BSModalModelForm): # pylint: disable=too-many-ancestors
|
||||
"""Form that uses django-bootstrap-modal-forms for satellite editing"""
|
||||
class Meta:
|
||||
model = SatelliteEntry
|
||||
fields = [
|
||||
'norad_cat_id', 'norad_follow_id', 'name', 'names', 'description', 'operator',
|
||||
'status', 'countries', 'website', 'dashboard_url', 'launched', 'deployed', 'decayed',
|
||||
'image', 'citation'
|
||||
]
|
||||
labels = {
|
||||
'norad_cat_id': _('Norad ID'),
|
||||
'norad_follow_id': _('Followed Norad ID'),
|
||||
'names': _('Other names'),
|
||||
'countries': _('Countries of Origin'),
|
||||
'launched': _('Launch Date'),
|
||||
'deployed': _('Deploy Date'),
|
||||
'decayed': _('Re-entry Date'),
|
||||
'description': _('Description'),
|
||||
'dashboard_url': _('Dashboard URL'),
|
||||
'operator': _('Owner/Operator'),
|
||||
}
|
||||
widgets = {'names': TextInput()}
|
||||
|
||||
|
||||
class SatelliteUpdateForm(BSModalModelForm): # pylint: disable=too-many-ancestors
|
||||
"""Form that uses django-bootstrap-modal-forms for satellite editing"""
|
||||
class Meta:
|
||||
model = SatelliteEntry
|
||||
fields = [
|
||||
'norad_cat_id', 'norad_follow_id', 'name', 'names', 'description', 'operator',
|
||||
'status', 'countries', 'website', 'dashboard_url', 'launched', 'deployed', 'decayed',
|
||||
'image', 'citation'
|
||||
]
|
||||
labels = {
|
||||
'norad_cat_id': _('Norad ID'),
|
||||
'norad_follow_id': _('Followed Norad ID'),
|
||||
'names': _('Other names'),
|
||||
'countries': _('Countries of Origin'),
|
||||
'launched': _('Launch Date'),
|
||||
'deployed': _('Deploy Date'),
|
||||
'decayed': _('Re-entry Date'),
|
||||
'description': _('Description'),
|
||||
'dashboard_url': _('Dashboard URL'),
|
||||
'operator': _('Owner/Operator'),
|
||||
}
|
||||
widgets = {'names': TextInput()}
|
||||
|
||||
|
||||
class MergeSatellitesForm(BSModalForm):
|
||||
"""Form that uses django-bootstrap-modal-forms for merging satellites"""
|
||||
primary_satellite = ModelChoiceField(
|
||||
label=_('Primary Satellite'),
|
||||
queryset=Satellite.objects.filter(
|
||||
associated_satellite__isnull=True, satellite_entry__approved=True
|
||||
),
|
||||
empty_label="Select the Primary Satellite"
|
||||
)
|
||||
associated_satellite = ModelChoiceField(
|
||||
label=_('Associated Satellite'),
|
||||
queryset=Satellite.objects.filter(
|
||||
associated_satellite__isnull=True, satellite_entry__approved=True
|
||||
),
|
||||
empty_label="Select the Associated Satellite"
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if any(self.errors):
|
||||
# If there are errors in forms validation no need for validating the formset
|
||||
return
|
||||
|
||||
cleaned_data = super().clean()
|
||||
primary_satellite = cleaned_data.get("primary_satellite")
|
||||
associated_satellite = cleaned_data.get("associated_satellite")
|
||||
|
||||
if primary_satellite == associated_satellite:
|
||||
self.add_error(
|
||||
'associated_satellite',
|
||||
ValidationError(
|
||||
_('Associated Satellite can not be the same with the Primary Satellite'),
|
||||
code='invalid'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = ['primary_satellite', 'associated_satellite']
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.core.cache import cache
|
||||
|
||||
"""Helper functions for SatNOGS DB"""
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWX'
|
||||
|
@ -7,10 +7,20 @@ LOWER = 'abcdefghijklmnopqrstuvwx'
|
|||
|
||||
|
||||
def gridsquare(lat, lng):
|
||||
if not (-180 <= lng < 180):
|
||||
return False
|
||||
if not (-90 <= lat < 90):
|
||||
return False
|
||||
"""Calculates a maidenhead grid square from a lat/long
|
||||
|
||||
Used when we get a SiDS submission, we want to store and display the
|
||||
location of the submitter as a grid square.
|
||||
|
||||
:returns: a string of the grid square, ie: EM69uf
|
||||
"""
|
||||
try:
|
||||
if not -180 <= lng < 180:
|
||||
return 'Unknown'
|
||||
if not -90 <= lat < 90:
|
||||
return 'Unknown'
|
||||
except TypeError:
|
||||
return 'Unknown'
|
||||
|
||||
adj_lat = lat + 90.0
|
||||
adj_lon = lng + 180.0
|
||||
|
@ -28,43 +38,21 @@ def gridsquare(lat, lng):
|
|||
grid_lon_subsq = LOWER[int(adj_lon_remainder / 5)]
|
||||
|
||||
qth = '{}'.format(
|
||||
grid_lon_sq + grid_lat_sq + grid_lon_field + grid_lat_field + grid_lon_subsq +
|
||||
grid_lat_subsq
|
||||
grid_lon_sq + grid_lat_sq + grid_lon_field + grid_lat_field + grid_lon_subsq
|
||||
+ grid_lat_subsq
|
||||
)
|
||||
|
||||
return qth
|
||||
|
||||
|
||||
def get_apikey(user):
|
||||
def get_api_token(user):
|
||||
"""If necessary, create, then return an API Token for a user
|
||||
|
||||
:param user: a SatNOGS DB User object
|
||||
:returns: user API token
|
||||
"""
|
||||
try:
|
||||
token = Token.objects.get(user=user)
|
||||
except Exception:
|
||||
except ObjectDoesNotExist:
|
||||
token = Token.objects.create(user=user)
|
||||
return token
|
||||
|
||||
|
||||
def cache_get_key(*args, **kwargs):
|
||||
import hashlib
|
||||
serialise = []
|
||||
for arg in args:
|
||||
serialise.append(str(arg))
|
||||
for key, arg in kwargs.items():
|
||||
serialise.append(str(key))
|
||||
serialise.append(str(arg))
|
||||
key = hashlib.md5("".join(serialise)).hexdigest()
|
||||
return key
|
||||
|
||||
|
||||
def cache_for(time):
|
||||
def decorator(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
key = cache_get_key(fn.__name__, *args, **kwargs)
|
||||
result = cache.get(key)
|
||||
if not result:
|
||||
result = fn(*args, **kwargs)
|
||||
cache.set(key, result, time)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from db.base.models import DemodData, Satellite, Telemetry
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Decode Satellite data'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional arguments
|
||||
parser.add_argument('satellite_identifiers', nargs='+', metavar='<Satellite Identifier>')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for item in options['satellite_identifiers']:
|
||||
satellites = Satellite.objects.all()
|
||||
for satellite in satellites:
|
||||
if satellite.has_telemetry_decoders:
|
||||
data = DemodData.objects.filter(satellite=satellite).filter(payload_decoded='')
|
||||
telemetry_decoders = Telemetry.objects.filter(satellite=satellite)
|
||||
|
||||
for obj in data:
|
||||
for option in telemetry_decoders:
|
||||
decoder_module = 'db.base.decoders.{0}'.format(option.decoder)
|
||||
decoder = __import__(decoder_module, fromlist='.')
|
||||
|
||||
with open(obj.payload_frame.path) as fp:
|
||||
frame = fp.read()
|
||||
|
||||
try:
|
||||
payload_decoded = decoder.decode_payload(
|
||||
frame, obj.timestamp, obj.data_id
|
||||
)
|
||||
except ValueError:
|
||||
obj.payload_decoded = ''
|
||||
obj.payload_telemetry = None
|
||||
obj.save()
|
||||
continue
|
||||
else:
|
||||
obj.payload_decoded = payload_decoded
|
||||
obj.payload_telemetry = option
|
||||
obj.save()
|
||||
break
|
|
@ -1,20 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from db.base.models import Satellite
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Delete selected Satellites'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional arguments
|
||||
parser.add_argument('norad_ids', nargs='+', metavar='<norad id>')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for norad_id in options['norad_ids']:
|
||||
try:
|
||||
Satellite.objects.get(norad_cat_id=norad_id).delete()
|
||||
self.stdout.write('Deleted satellite {}.'.format(norad_id))
|
||||
continue
|
||||
except Exception:
|
||||
self.stderr.write('Satellite with Identifier {} does not exist'.format(norad_id))
|
|
@ -1,62 +0,0 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management.base import BaseCommand
|
||||
from pytz import timezone
|
||||
|
||||
from db.base.models import DemodData, Satellite, TransmitterEntry
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Fetch Satellite data from Network'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
apiurl = settings.NETWORK_API_ENDPOINT
|
||||
data_url = "{0}data".format(apiurl)
|
||||
start_date = datetime.utcnow() - timedelta(days=int(settings.DATA_FETCH_DAYS))
|
||||
start_date = datetime.strftime(start_date, '%Y-%m-%dT%H:%M:%SZ')
|
||||
params = {'start': start_date}
|
||||
response = requests.get(data_url, params=params)
|
||||
|
||||
satellites = Satellite.objects.all()
|
||||
|
||||
for obj in response.json():
|
||||
norad_cat_id = obj['norad_cat_id']
|
||||
data_id = obj['id']
|
||||
station = obj['station_name']
|
||||
lat = obj['station_lat']
|
||||
lng = obj['station_lng']
|
||||
try:
|
||||
satellite = satellites.get(norad_cat_id=norad_cat_id)
|
||||
except Satellite.DoesNotExist:
|
||||
continue
|
||||
try:
|
||||
transmitter = TransmitterEntry.objects.get(
|
||||
uuid=obj['transmitter'], created=obj['transmitter_created']
|
||||
)
|
||||
except TransmitterEntry.DoesNotExist:
|
||||
transmitter = None
|
||||
|
||||
DemodData.objects.filter(data_id=data_id).delete()
|
||||
|
||||
for demoddata in obj['demoddata']:
|
||||
payload_url = demoddata['payload_demod']
|
||||
timestamp = datetime.strptime(
|
||||
payload_url.split('/')[-1].split('_')[0], '%Y%m%dT%H%M%SZ'
|
||||
).replace(tzinfo=timezone('UTC'))
|
||||
frame = str(requests.get(payload_url).json())
|
||||
payload_frame = ContentFile(frame, name='network')
|
||||
|
||||
DemodData.objects.create(
|
||||
satellite=satellite,
|
||||
transmitter=transmitter,
|
||||
data_id=data_id,
|
||||
payload_frame=payload_frame,
|
||||
timestamp=timestamp,
|
||||
source='network',
|
||||
station=station,
|
||||
lat=lat,
|
||||
lng=lng
|
||||
)
|
|
@ -1,19 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from db.base.tasks import update_satellite
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Updates/Inserts Name for certain Satellites'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional arguments
|
||||
parser.add_argument('norad_ids', nargs='+', metavar='<norad id>')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for norad_id in options['norad_ids']:
|
||||
try:
|
||||
update_satellite(int(norad_id), update_name=True, update_tle=False)
|
||||
except LookupError:
|
||||
self.stderr.write('Satellite {} not found in Celestrak'.format(norad_id))
|
||||
continue
|
|
@ -1,8 +1,10 @@
|
|||
"""SatNOGS DB django management command to initialize a new database"""
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""django management command to initialize a new database"""
|
||||
help = 'Create initial fixtures'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
@ -10,9 +12,12 @@ class Command(BaseCommand):
|
|||
self.stdout.write("Creating database...")
|
||||
call_command('migrate')
|
||||
|
||||
# Initial data
|
||||
# Initial data
|
||||
self.stdout.write("Creating fixtures...")
|
||||
call_command('loaddata', 'modes')
|
||||
call_command('loaddata', 'operators')
|
||||
call_command('loaddata', 'satelliteidentifiers')
|
||||
call_command('loaddata', 'satelliteentries')
|
||||
call_command('loaddata', 'satellites')
|
||||
call_command('loaddata', 'transmitters')
|
||||
call_command('loaddata', 'telemetries')
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
"""SatNOGS DB django management command to update TLE entries"""
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from db.base.tasks import update_all_tle
|
||||
from db.base.tasks import update_tle_sets
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""django management command to update TLE entries"""
|
||||
help = 'Update TLEs for existing Satellites'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
update_all_tle()
|
||||
update_tle_sets()
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import django.db.models.deletion
|
||||
import shortuuidfield.fields
|
||||
import django.core.validators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Mode',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=10)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Satellite',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('norad_cat_id', models.PositiveIntegerField()),
|
||||
('name', models.CharField(max_length=45)),
|
||||
('names', models.TextField(blank=True)),
|
||||
('image', models.ImageField(upload_to=b'satellites', blank=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Transmitter',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('uuid', shortuuidfield.fields.ShortUUIDField(db_index=True, unique=True, max_length=22, editable=False, blank=True)),
|
||||
('description', models.TextField()),
|
||||
('alive', models.BooleanField(default=True)),
|
||||
('uplink_low', models.PositiveIntegerField(null=True, blank=True)),
|
||||
('uplink_high', models.PositiveIntegerField(null=True, blank=True)),
|
||||
('downlink_low', models.PositiveIntegerField(null=True, blank=True)),
|
||||
('downlink_high', models.PositiveIntegerField(null=True, blank=True)),
|
||||
('invert', models.BooleanField(default=False)),
|
||||
('baud', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)])),
|
||||
('approved', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Suggestion',
|
||||
fields=[
|
||||
('transmitter_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='base.Transmitter')),
|
||||
('citation', models.CharField(max_length=255, blank=True)),
|
||||
],
|
||||
bases=('base.transmitter',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transmitter',
|
||||
name='mode',
|
||||
field=models.ForeignKey(related_name='transmitters', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='base.Mode', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transmitter',
|
||||
name='satellite',
|
||||
field=models.ForeignKey(related_name='transmitters', to='base.Satellite', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='suggestion',
|
||||
name='transmitter',
|
||||
field=models.ForeignKey(related_name='suggestions', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='base.Transmitter', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='suggestion',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True),
|
||||
),
|
||||
]
|
|
@ -1,14 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2018-01-07 12:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import db.base.models
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import shortuuidfield.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import db.base.models
|
||||
|
||||
# Functions from the following migrations need manual copying.
|
||||
# Move them and any dependencies into this file, then update the
|
||||
|
@ -17,7 +15,7 @@ import shortuuidfield.fields
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [(b'base', '0001_initial'), (b'base', '0002_auto_20150908_2054'), (b'base', '0003_auto_20160504_2104'), (b'base', '0004_auto_20170302_1641'), (b'base', '0005_demoddata_observer'), (b'base', '0006_auto_20170323_1715'), (b'base', '0007_satellite_status'), (b'base', '0008_satellite_description'), (b'base', '0009_auto_20180103_1931')]
|
||||
replaces = [('base', '0001_initial'), ('base', '0002_auto_20150908_2054'), ('base', '0003_auto_20160504_2104'), ('base', '0004_auto_20170302_1641'), ('base', '0005_demoddata_observer'), ('base', '0006_auto_20170323_1715'), ('base', '0007_satellite_status'), ('base', '0008_satellite_description'), ('base', '0009_auto_20180103_1931')]
|
||||
|
||||
initial = True
|
||||
|
||||
|
@ -40,10 +38,10 @@ class Migration(migrations.Migration):
|
|||
('norad_cat_id', models.PositiveIntegerField()),
|
||||
('name', models.CharField(max_length=45)),
|
||||
('names', models.TextField(blank=True)),
|
||||
('image', models.ImageField(blank=True, help_text=b'Ideally: 250x250', upload_to=b'satellites')),
|
||||
('image', models.ImageField(blank=True, help_text='Ideally: 250x250', upload_to='satellites')),
|
||||
('tle1', models.CharField(blank=True, max_length=200)),
|
||||
('tle2', models.CharField(blank=True, max_length=200)),
|
||||
('status', models.CharField(choices=[(b'alive', b'alive'), (b'dead', b'dead'), (b're-entered', b're-entered')], default=b'alive', max_length=10)),
|
||||
('status', models.CharField(choices=[('alive', 'alive'), ('dead', 'dead'), ('re-entered', 're-entered')], default='alive', max_length=10)),
|
||||
('description', models.TextField(blank=True)),
|
||||
],
|
||||
options={
|
||||
|
@ -105,8 +103,8 @@ class Migration(migrations.Migration):
|
|||
('payload_decoded', models.TextField(blank=True)),
|
||||
('payload_frame', models.FileField(blank=True, null=True, upload_to=db.base.models._name_payload_frame)),
|
||||
('satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='telemetry_data', to='base.Satellite')),
|
||||
('source', models.CharField(choices=[(b'manual', b'manual'), (b'network', b'network'), (b'sids', b'sids')], default=b'sids', max_length=7)),
|
||||
('station', models.CharField(default=b'Unknown', max_length=45)),
|
||||
('source', models.CharField(choices=[('manual', 'manual'), ('network', 'network'), ('sids', 'sids')], default='sids', max_length=7)),
|
||||
('station', models.CharField(default='Unknown', max_length=45)),
|
||||
('timestamp', models.DateTimeField(null=True)),
|
||||
],
|
||||
options={
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='image',
|
||||
field=models.ImageField(help_text=b'Ideally: 250x250', upload_to=b'satellites', blank=True),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-03-04 08:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.6 on 2016-05-04 21:04
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0002_auto_20150908_2054'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DemodData',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('data_id', models.PositiveIntegerField()),
|
||||
('payload', jsonfield.fields.JSONField(default=dict)),
|
||||
('transmitter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.Transmitter')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='telemetry_decoder',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='telemetry_schema',
|
||||
field=jsonfield.fields.JSONField(blank=True, default=dict),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-09-19 00:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-03-02 16:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import db.base.models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0003_auto_20160504_2104'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Telemetry',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=45)),
|
||||
('schema', models.TextField(blank=True)),
|
||||
('decoder', models.CharField(blank=True, max_length=20)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['satellite__norad_cat_id'],
|
||||
'verbose_name_plural': 'Telemetries',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='demoddata',
|
||||
options={'ordering': ['-timestamp']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='satellite',
|
||||
options={'ordering': ['norad_cat_id']},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='demoddata',
|
||||
name='payload',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='satellite',
|
||||
name='telemetry_decoder',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='satellite',
|
||||
name='telemetry_schema',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='lat',
|
||||
field=models.FloatField(default=0, validators=[django.core.validators.MaxValueValidator(90), django.core.validators.MinValueValidator(-90)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='lng',
|
||||
field=models.FloatField(default=0, validators=[django.core.validators.MaxValueValidator(180), django.core.validators.MinValueValidator(-180)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='payload_decoded',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='payload_frame',
|
||||
field=models.FileField(blank=True, null=True, upload_to=db.base.models._name_payload_frame),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='satellite',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='telemetry_data', to='base.Satellite'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='source',
|
||||
field=models.CharField(choices=[(b'manual', b'manual'), (b'network', b'network'), (b'sids', b'sids')], default=b'sids', max_length=7),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='station',
|
||||
field=models.CharField(default=b'Unknown', max_length=45),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='demoddata',
|
||||
name='data_id',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='demoddata',
|
||||
name='transmitter',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Transmitter'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='telemetry',
|
||||
name='satellite',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='telemetries', to='base.Satellite'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='payload_telemetry',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base.Telemetry'),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-12-15 11:30
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
@ -20,7 +18,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='transmitter',
|
||||
name='type',
|
||||
field=models.CharField(choices=[(b'Transmitter', b'Transmitter'), (b'Transceiver', b'Transceiver'), (b'Transponder', b'Transponder')], default=b'Transmitter', max_length=11),
|
||||
field=models.CharField(choices=[('Transmitter', 'Transmitter'), ('Transceiver', 'Transceiver'), ('Transponder', 'Transponder')], default='Transmitter', max_length=11),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transmitter',
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-14 18:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0004_auto_20170302_1641'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='observer',
|
||||
field=models.CharField(blank=True, max_length=60),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2019-01-19 16:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-03-23 17:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0005_demoddata_observer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='tle1',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='tle2',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2019-01-21 13:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.6 on 2017-05-08 15:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0006_auto_20170323_1715'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='status',
|
||||
field=models.CharField(choices=[(b'alive', b'alive'), (b'dead', b'dead'), (b're-entered', b're-entered')], default=b'alive', max_length=10),
|
||||
),
|
||||
]
|
|
@ -1,9 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2019-03-29 19:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def add_suggestion_permissions(apps, schema_editor):
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-16 21:09
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0007_satellite_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='description',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
|
@ -1,13 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2019-04-04 04:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db.models import Max
|
||||
import django.utils.timezone
|
||||
import shortuuidfield.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
def from_alive_to_status(apps, schema_editor):
|
||||
|
@ -78,7 +76,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='citation',
|
||||
field=models.CharField(default=b'CITATION NEEDED - https://xkcd.com/285/', max_length=512),
|
||||
field=models.CharField(default='CITATION NEEDED - https://xkcd.com/285/', max_length=512),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
|
@ -93,7 +91,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='status',
|
||||
field=models.CharField(choices=[(b'active', b'active'), (b'inactive', b'inactive'), (b'invalid', b'invalid')], default=b'active', max_length=8),
|
||||
field=models.CharField(choices=[('active', 'active'), ('inactive', 'inactive'), ('invalid', 'invalid')], default='active', max_length=8),
|
||||
),
|
||||
migrations.RunPython(from_alive_to_status, reverse_from_alive_to_status),
|
||||
migrations.RemoveField(
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2018-01-03 19:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.utils.timezone import now
|
||||
|
||||
|
||||
def move_payloads(apps, schema_editor):
|
||||
DemodData = apps.get_model('base', 'DemodData')
|
||||
for demod in DemodData.objects.all():
|
||||
try:
|
||||
folder = 'payload_frames/{0}/{1}/{2}/'.format(demod.timestamp.year,
|
||||
demod.timestamp.month,
|
||||
demod.timestamp.day)
|
||||
except AttributeError:
|
||||
folder = 'payload_frames/{0}/{1}/{2}/'.format(now().year, now().month, now().day)
|
||||
fullpath = '{0}/{1}'.format(settings.MEDIA_ROOT, folder)
|
||||
if not os.path.exists(fullpath):
|
||||
os.makedirs(fullpath)
|
||||
filename = demod.payload_frame.name.split('/')[-1]
|
||||
new_name = '{0}{1}'.format(folder, filename)
|
||||
new_path = '{0}/{1}'.format(settings.MEDIA_ROOT, new_name)
|
||||
os.rename(demod.payload_frame.path, new_path)
|
||||
demod.payload_frame.name = new_name
|
||||
demod.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0008_satellite_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(move_payloads),
|
||||
]
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-04-20 23:06
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.20 on 2019-04-21 01:43
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 1.11.20 on 2019-07-02 05:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0010_auto_20190421_0143'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='service',
|
||||
field=models.CharField(choices=[('Aeronautical', 'Aeronautical'), ('Amateur', 'Amateur'), ('Broadcasting', 'Broadcasting'), ('Earth Exploration', 'Earth Exploration'), ('Fixed', 'Fixed'), ('Inter-satellite', 'Inter-satellite'), ('Maritime', 'Maritime'), ('Meteorological', 'Meteorological'), ('Mobile', 'Mobile'), ('Radiolocation', 'Radiolocation'), ('Radionavigational', 'Radionavigational'), ('Space Operation', 'Space Operation'), ('Space Research', 'Space Research'), ('Standard Frequency and Time Signal', 'Standard Frequency and Time Signal'), ('Unknown', 'Unknown')], default='Unknown', max_length=34),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.6 on 2019-11-08 18:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0011_transmitterentry_service'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='transmitterentry',
|
||||
old_name='mode',
|
||||
new_name='downlink_mode',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 2.2.6 on 2019-11-08 18:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def copy_field(apps, schema_editor):
|
||||
models = apps.get_model('base', 'TransmitterEntry')
|
||||
modes = apps.get_model('base', 'Mode')
|
||||
for transmitter in models.objects.all().iterator():
|
||||
if transmitter.type != "Transmitter":
|
||||
transmitter.uplink_mode = transmitter.downlink_mode
|
||||
if transmitter.invert == True:
|
||||
if transmitter.downlink_mode.name == "USB":
|
||||
transmitter.uplink_mode = modes.objects.get(name="LSB")
|
||||
elif transmitter.downlink_mode.name == "LSB":
|
||||
transmitter.uplink_mode = modes.objects.get(name="USB")
|
||||
|
||||
transmitter.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0012_auto_20191108_1840'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='uplink_mode',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitter_uplink_entries', to='base.Mode'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='downlink_mode',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitter_downlink_entries', to='base.Mode'),
|
||||
),
|
||||
migrations.RunPython(copy_field, reverse_code=migrations.RunPython.noop),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.9 on 2020-02-01 20:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0013_auto_20191108_1842'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='mode',
|
||||
name='name',
|
||||
field=models.CharField(max_length=12, unique=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.12 on 2020-04-07 09:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0014_increase_char_limit_on_mode_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='dashboard_url',
|
||||
field=models.URLField(blank=True, null=True, max_length=200),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.10 on 2020-04-10 11:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0015_satellite_dashboard_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='mode',
|
||||
name='name',
|
||||
field=models.CharField(max_length=25, unique=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 2.2.11 on 2020-05-20 14:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import db.base.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('base', '0016_increase_mode_name_char_limit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExportedFrameset',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('exported_file', models.FileField(blank=True, null=True, upload_to=db.base.models._name_exported_frames)),
|
||||
('start', models.DateTimeField(blank=True, null=True)),
|
||||
('end', models.DateTimeField(blank=True, null=True)),
|
||||
('satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.Satellite')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.12 on 2020-05-20 16:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0017_exported_frameset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Artifact',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('artifact_file', models.FileField(blank=True, null=True, upload_to='artifacts/')),
|
||||
('network_obs_id', models.BigIntegerField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,50 @@
|
|||
# Generated by Django 2.2.14 on 2020-07-15 12:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0018_artifact'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Operator',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('names', models.TextField(blank=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('website', models.URLField(blank=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='countries',
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=746, multiple=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='deployed',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='launched',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='website',
|
||||
field=models.URLField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='operator',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_operator', to='base.Operator'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,106 @@
|
|||
# Generated by Django 2.2.14 on 2020-08-02 18:04
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0019_satellite_details'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operator',
|
||||
name='website',
|
||||
field=models.URLField(blank=True, validators=[django.core.validators.URLValidator(regex="(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$", schemes=['http', 'https'])]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='dashboard_url',
|
||||
field=models.URLField(blank=True, null=True, validators=[django.core.validators.URLValidator(regex="(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$", schemes=['http', 'https'])]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='website',
|
||||
field=models.URLField(blank=True, validators=[django.core.validators.URLValidator(regex="(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$", schemes=['http', 'https'])]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='baud',
|
||||
field=models.FloatField(blank=True, help_text='The number of modulated symbols that the transmitter sends every second', null=True, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='citation',
|
||||
field=models.CharField(default='CITATION NEEDED - https://xkcd.com/285/', help_text='A reference (preferrably URL) for this entry or edit', max_length=512),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='created',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Timestamp for this entry or edit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='description',
|
||||
field=models.TextField(help_text='Short description for this entry, like: UHF 9k6 AFSK Telemetry'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='downlink_drift',
|
||||
field=models.IntegerField(blank=True, help_text='Transmitter drift from the published downlink frequency, stored in parts per billion (PPB)', null=True, validators=[django.core.validators.MinValueValidator(-99999), django.core.validators.MaxValueValidator(99999)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='downlink_high',
|
||||
field=models.BigIntegerField(blank=True, help_text='Frequency (in Hz) for the top of the downlink range for a transponder', null=True, validators=[django.core.validators.MinValueValidator(0, message='Ensure this value is greater than or equal to 0Hz'), django.core.validators.MaxValueValidator(40000000000, message='Ensure this value is less than or equal to 40Ghz')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='downlink_low',
|
||||
field=models.BigIntegerField(blank=True, help_text='Frequency (in Hz) for the downlink, or bottom of the downlink range for a transponder', null=True, validators=[django.core.validators.MinValueValidator(0, message='Ensure this value is greater than or equal to 0Hz'), django.core.validators.MaxValueValidator(40000000000, message='Ensure this value is less than or equal to 40Ghz')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='downlink_mode',
|
||||
field=models.ForeignKey(blank=True, help_text='Modulation mode for the downlink', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitter_downlink_entries', to='base.Mode'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='invert',
|
||||
field=models.BooleanField(default=False, help_text='True if this is an inverted transponder'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='service',
|
||||
field=models.CharField(choices=[('Aeronautical', 'Aeronautical'), ('Amateur', 'Amateur'), ('Broadcasting', 'Broadcasting'), ('Earth Exploration', 'Earth Exploration'), ('Fixed', 'Fixed'), ('Inter-satellite', 'Inter-satellite'), ('Maritime', 'Maritime'), ('Meteorological', 'Meteorological'), ('Mobile', 'Mobile'), ('Radiolocation', 'Radiolocation'), ('Radionavigational', 'Radionavigational'), ('Space Operation', 'Space Operation'), ('Space Research', 'Space Research'), ('Standard Frequency and Time Signal', 'Standard Frequency and Time Signal'), ('Unknown', 'Unknown')], default='Unknown', help_text='The published usage category for this transmitter', max_length=34),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('active', 'active'), ('inactive', 'inactive'), ('invalid', 'invalid')], default='active', help_text='Functional state of this transmitter', max_length=8),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='uplink_drift',
|
||||
field=models.IntegerField(blank=True, help_text='Receiver drift from the published uplink frequency, stored in parts per billion (PPB)', null=True, validators=[django.core.validators.MinValueValidator(-99999), django.core.validators.MaxValueValidator(99999)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='uplink_high',
|
||||
field=models.BigIntegerField(blank=True, help_text='Frequency (in Hz) for the top of the uplink range for a transponder', null=True, validators=[django.core.validators.MinValueValidator(0, message='Ensure this value is greater than or equal to 0Hz'), django.core.validators.MaxValueValidator(40000000000, message='Ensure this value is less than or equal to 40Ghz')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='uplink_low',
|
||||
field=models.BigIntegerField(blank=True, help_text='Frequency (in Hz) for the uplink, or bottom of the uplink range for a transponder', null=True, validators=[django.core.validators.MinValueValidator(0, message='Ensure this value is greater than or equal to 0Hz'), django.core.validators.MaxValueValidator(40000000000, message='Ensure this value is less than or equal to 40Ghz')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='uplink_mode',
|
||||
field=models.ForeignKey(blank=True, help_text='Modulation mode for the uplink', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitter_uplink_entries', to='base.Mode'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.15 on 2020-08-08 17:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0020_auto_20200802_1804'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='demoddata',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(db_index=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,53 @@
|
|||
# Generated by Django 2.2.14 on 2020-08-03 02:04
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0021_auto_20200808_1725'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tle',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tle0', models.CharField(blank=True, max_length=69, validators=[django.core.validators.MinLengthValidator(1), django.core.validators.MaxLengthValidator(69)])),
|
||||
('tle1', models.CharField(blank=True, max_length=69, validators=[django.core.validators.MinLengthValidator(69), django.core.validators.MaxLengthValidator(69)])),
|
||||
('tle2', models.CharField(blank=True, max_length=69, validators=[django.core.validators.MinLengthValidator(69), django.core.validators.MaxLengthValidator(69)])),
|
||||
('tle_source', models.CharField(blank=True, max_length=300)),
|
||||
('updated', models.DateTimeField(auto_now=True)),
|
||||
('satellite', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tles', to='base.Satellite')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LatestTle',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('base.tle',),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tle',
|
||||
index=models.Index(fields=['-updated'], name='base_tle_updated_8936f7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tle',
|
||||
index=models.Index(fields=['tle1', 'tle2', 'tle_source', 'satellite'], name='base_tle_tle1_30ea48_idx'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='tle',
|
||||
constraint=models.UniqueConstraint(fields=('tle1', 'tle2', 'tle_source', 'satellite'), name='unique_entry_from_source_for_satellite'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.14 on 2020-08-05 04:07
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0022_add_tle_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='tle',
|
||||
options={'ordering': ['-updated'], 'permissions': [('access_all_tles', 'Access all TLEs')]},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.15 on 2020-09-06 15:18
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0023_add_tle_model_permission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='coordination',
|
||||
field=models.CharField(blank=True, choices=[('ITU Requested', 'ITU Requested'), ('ITU Rejected', 'ITU Rejected'), ('ITU Coordinated', 'ITU Coordinated'), ('IARU Requested', 'IARU Requested'), ('IARU Rejected', 'IARU Rejected'), ('IARU Coordinated', 'IARU Coordinated'), ('Uncoordinated', 'Uncoordinated')], default='', help_text='Frequency coordination status for this transmitter', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='coordination_url',
|
||||
field=models.URLField(blank=True, help_text='URL for more details on this frequency coordination', validators=[django.core.validators.URLValidator(regex="(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$", schemes=['http', 'https'])]),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 2.2.14 on 2020-09-14 15:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0024_frequency_coordination_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='satellite',
|
||||
name='tle1',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='satellite',
|
||||
name='tle2',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='satellite',
|
||||
name='tle_source',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.14 on 2020-09-17 09:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0025_remove_satellite_tle_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='satellite',
|
||||
name='norad_follow_id',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.14 on 2020-09-21 16:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0026_add_satellite_norad_follow_id_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='satellite',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('alive', 'alive'), ('dead', 'dead'), ('future', 'future'), ('re-entered', 're-entered')], default='alive', max_length=10),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 2.2.14 on 2020-09-22 14:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0027_add_future_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveConstraint(
|
||||
model_name='tle',
|
||||
name='unique_entry_from_source_for_satellite',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='tle',
|
||||
name='base_tle_tle1_30ea48_idx',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 2.2.14 on 2020-09-22 14:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0028_remove_tle_constraint'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LatestTleSet',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_modified', models.DateTimeField(auto_now=True)),
|
||||
('latest', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='latest', to='base.Tle')),
|
||||
('latest_distributable', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='latest_distributable', to='base.Tle')),
|
||||
('satellite', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='latest_tle_set', to='base.Satellite')),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='LatestTle',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.16 on 2020-09-23 12:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0029_change_latest_tle_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='telemetry',
|
||||
name='decoder',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.16 on 2020-10-11 14:09
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0030_increase_size_of_decoder_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tle',
|
||||
name='satellite',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tles', to='base.Satellite'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.5 on 2021-01-20 00:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0031_set_tle_ondelete_null'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='version',
|
||||
field=models.CharField(blank=True, max_length=45),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.5 on 2021-01-30 04:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0032_sids_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='observation_id',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.5 on 2021-02-04 09:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0033_add_observations_id_in_demoddata_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='demoddata',
|
||||
name='station_id',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.5 on 2021-03-07 15:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0034_add_station_id_in_demoddata_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='transmitterentry',
|
||||
old_name='user',
|
||||
new_name='created_by',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.1.5 on 2021-03-07 17:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0035_rename_user_field_on_transmitter_entry_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='transmitterentry',
|
||||
old_name='reviewed',
|
||||
new_name='is_reviewed',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,86 @@
|
|||
# Generated by Django 3.1.5 on 2021-03-08 22:59
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
def add_review_details(apps, schema_editor):
|
||||
TransmitterEntry = apps.get_model('base', 'TransmitterEntry')
|
||||
orphans = TransmitterEntry.objects.filter(created_by__isnull=True, is_reviewed=True)
|
||||
for entry in orphans:
|
||||
entry.reviewed = entry.created
|
||||
entry.save()
|
||||
ids = []
|
||||
non_orphans = TransmitterEntry.objects.filter(created_by__isnull=False, is_reviewed=True).order_by('created')
|
||||
for entry in non_orphans:
|
||||
if entry.id in ids:
|
||||
entry.delete()
|
||||
continue
|
||||
next_entries = TransmitterEntry.objects.filter(created_by__isnull=False, is_reviewed=True).filter(uuid=entry.uuid).filter(created__gt=entry.created).order_by('created')
|
||||
for next_entry in next_entries:
|
||||
if (entry.uuid == next_entry.uuid and entry.description == next_entry.description and
|
||||
entry.status == next_entry.status and entry.type == next_entry.type and entry.uplink_low == next_entry.uplink_low and
|
||||
entry.uplink_high == next_entry.uplink_high and entry.uplink_drift == next_entry.uplink_drift and
|
||||
entry.downlink_low == next_entry.downlink_low and entry.downlink_high == next_entry.downlink_high and
|
||||
entry.downlink_drift == next_entry.downlink_drift and entry.downlink_mode == next_entry.downlink_mode and
|
||||
entry.uplink_mode == next_entry.uplink_mode and entry.invert == next_entry.invert and entry.baud == next_entry.baud and
|
||||
entry.satellite == next_entry.satellite and entry.reviewed == next_entry.reviewed and entry.approved == next_entry.approved and
|
||||
entry.citation == next_entry.citation and entry.service == next_entry.service and entry.coordination == next_entry.coordination and
|
||||
entry.coordination_url == next_entry.coordination_url):
|
||||
ids.append(next_entry.id)
|
||||
entry.reviewed = next_entry.created
|
||||
entry.reviewer = next_entry.created_by
|
||||
entry.save()
|
||||
break
|
||||
else:
|
||||
entry.reviewed = entry.created
|
||||
entry.reviewer = entry.created_by
|
||||
entry.save()
|
||||
|
||||
|
||||
def remove_review_details(apps, schema_editor):
|
||||
TransmitterEntry = apps.get_model('base', 'TransmitterEntry')
|
||||
non_orphans = TransmitterEntry.objects.filter(created_by__isnull=False, is_reviewed=True).order_by('created')
|
||||
for entry in non_orphans:
|
||||
entry.pk = None
|
||||
entry.created = entry.reviewed
|
||||
entry.created_by = entry.reviewer
|
||||
entry.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('base', '0036_rename_reviewed_field_on_transmitter_entry_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='reviewed',
|
||||
field=models.DateTimeField(blank=True, help_text='Timestamp of review', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transmitterentry',
|
||||
name='reviewer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_transmitters', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='created',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Timestamp of creation/edit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transmitterentry',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_transmitters', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='transmitterentry',
|
||||
unique_together={('uuid', 'reviewed')},
|
||||
),
|
||||
migrations.RunPython(add_review_details, remove_review_details),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.1.5 on 2021-03-10 22:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0037_add_reviewer_and_date_fields_on_transmitter_entry_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='transmitterentry',
|
||||
name='is_reviewed',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 3.1.5 on 2021-04-13 23:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0038_remove_is_reviewed_field_from_transmitter_entry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Satellite',
|
||||
new_name='SatelliteEntry',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='satelliteentry',
|
||||
options={'ordering': ['norad_cat_id'], 'verbose_name_plural': 'Satellite Entries'},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,108 @@
|
|||
# Generated by Django 3.1.8 on 2021-04-15 01:56
|
||||
|
||||
import db.base.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
def initialize_satellites(apps, schema_editor):
|
||||
SatelliteEntry = apps.get_model('base', 'SatelliteEntry')
|
||||
SatelliteIdentifier = apps.get_model('base', 'SatelliteIdentifier')
|
||||
Satellite = apps.get_model('base', 'Satellite')
|
||||
satellite_entries = SatelliteEntry.objects.all()
|
||||
for satellite_entry in satellite_entries:
|
||||
satellite_identifier = SatelliteIdentifier.objects.create()
|
||||
created = django.utils.timezone.now()
|
||||
satellite_entry.satellite_identifier = satellite_identifier
|
||||
satellite_entry.created = created
|
||||
satellite_entry.reviewed = created
|
||||
satellite_entry.approved = True
|
||||
satellite_entry.save()
|
||||
Satellite.objects.create(satellite_identifier=satellite_identifier, satellite_entry=satellite_entry)
|
||||
|
||||
|
||||
def reverse_initialize_satellites(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('base', '0039_rename_transmitter_satellite_field_to_satellite_entry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SatelliteIdentifier',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('sat_id', models.CharField(default=db.base.models.generate_sat_id, max_length=24, unique=True, validators=[db.base.models.validate_sat_id])),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SatelliteSuggestion',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'permissions': (('approve', 'Can approve/reject satellite suggestions'),),
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('base.satelliteentry',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='approved',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='citation',
|
||||
field=models.CharField(default='CITATION NEEDED - https://xkcd.com/285/', help_text='A reference (preferrably URL) for this entry or edit', max_length=512),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='created',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Timestamp of creation/edit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_satellites', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='reviewed',
|
||||
field=models.DateTimeField(blank=True, help_text='Timestamp of review', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='reviewer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_satellites', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Satellite',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_modified', models.DateTimeField(auto_now=True)),
|
||||
('associated_satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='associated_with', to='base.satellite')),
|
||||
('satellite_entry', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.satelliteentry')),
|
||||
('satellite_identifier', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='satellite', to='base.satelliteidentifier')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='satelliteentry',
|
||||
name='satellite_identifier',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='satellite_entries', to='base.satelliteidentifier'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='satelliteentry',
|
||||
unique_together={('satellite_identifier', 'reviewed')},
|
||||
),
|
||||
migrations.RunPython(initialize_satellites, reverse_initialize_satellites),
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue