diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..f70c620d Binary files /dev/null and b/.DS_Store differ diff --git a/.flake8 b/.flake8 index f80feb11..be59c58b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] exclude = .git,__pycache__,backend/settings*,*/migrations/*,venv,.venv -ignore = E302,E731 +ignore = E302,E731,W503 max-complexity = 15 max-line-length = 120 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a168702a..b95b184d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,61 +2,59 @@ name: Github Actions on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: test: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: ['3.10'] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pipenv wheel - pipenv sync - - - name: Run Tests - run: | - pipenv run python manage.py test -v2 - env: - DJANGO_SETTINGS_MODULE: backend.settings_test + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pipenv wheel + pipenv sync + + - name: Run Tests + run: | + pipenv run python manage.py test -v2 + env: + DJANGO_SETTINGS_MODULE: backend.settings_test lint: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: ['3.10'] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pipenv wheel - pipenv sync - - - name: Run Lint - run: | - pipenv run flake8 - pipenv run pylint_runner - env: - DJANGO_SETTINGS_MODULE: backend.settings_test + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pipenv wheel + pipenv sync + + - name: Run Lint + run: | + pipenv run flake8 + pipenv run pylint_runner test-mysql: services: @@ -85,33 +83,33 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: ['3.10'] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pipenv wheel - pipenv sync - pipenv run python -m pip install mysqlclient - - - name: Run Celery - run: | - pipenv run celery -A backend worker -l info & - env: - DJANGO_SETTINGS_MODULE: backend.settings_test_full_mysql - - - name: Run Tests - run: | - pipenv run python manage.py test -v2 - env: - DJANGO_SETTINGS_MODULE: backend.settings_test_full_mysql + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pipenv wheel + pipenv sync + pipenv run python -m pip install mysqlclient + + - name: Run Celery + run: | + pipenv run celery -A backend worker -l info & + env: + DJANGO_SETTINGS_MODULE: backend.settings_test_full_mysql + + - name: Run Tests + run: | + pipenv run python manage.py test -v2 + env: + DJANGO_SETTINGS_MODULE: backend.settings_test_full_mysql test-full: services: @@ -121,10 +119,10 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: password options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 ports: - 5432:5432 @@ -136,39 +134,39 @@ jobs: sonic: image: radialapps/sonic:v1.2.1 ports: - - 1491:1491 + - 1491:1491 runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: ['3.10'] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install pipenv wheel - pipenv sync - pipenv run python -m pip install coverage psycopg2-binary - - - name: Run Celery - run: | - pipenv run celery -A backend worker -l info & - env: - DJANGO_SETTINGS_MODULE: backend.settings_test_full - - - name: Run Tests - run: | - pipenv run python -m coverage run manage.py test -v2 - pipenv run python -m coverage xml -i - env: - DJANGO_SETTINGS_MODULE: backend.settings_test_full - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pipenv wheel + pipenv sync + pipenv run python -m pip install coverage psycopg2-binary + + - name: Run Celery + run: | + pipenv run celery -A backend worker -l info & + env: + DJANGO_SETTINGS_MODULE: backend.settings_test_full + + - name: Run Tests + run: | + pipenv run python -m coverage run manage.py test -v2 + pipenv run python -m coverage xml -i + env: + DJANGO_SETTINGS_MODULE: backend.settings_test_full + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 diff --git a/.gitignore b/.gitignore index e96e0a2a..8afd815c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ google_client_secret.json +chatbot_logs.json .vscode upload/static htmlcov @@ -109,3 +110,4 @@ venv.bak/ # mypy .mypy_cache/ + diff --git a/.pylintrc b/.pylintrc index b8d2b8f5..9faeba91 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,4 +4,5 @@ ignore=.git,__pycache__,backend,migrations,venv,.venv [MESSAGES CONTROL] max-line-length=120 -disable=C0111,C0103,R0903,R0901,R0801,W0221,E1101,E5142,C0209,R1732,R1729,R0402,R1721,W0613,E5110,W1514,W0622 +max-locals=20 +disable=C0111,C0103,R0903,R0901,R0801,W0221,E1101,E5142,C0209,R1732,R1729,R0402,R1721,W0613,E5110,W1514,W0622,W0104,W0703,R0911 diff --git a/Pipfile b/Pipfile index 37ababbc..8b4ecd8b 100644 --- a/Pipfile +++ b/Pipfile @@ -4,6 +4,7 @@ url = "https://pypi.org/simple" verify_ssl = true [dev-packages] +autopep8 = "*" [packages] asonic = "*" @@ -40,3 +41,4 @@ django-elasticsearch-dsl = "*" elasticsearch-dsl = "*" elasticsearch = "*" google-api-python-client = "*" +django-cors-headers = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 6dd32e1a..446ef614 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "70c6f8dff73e43354dde0402db2ed0e82776fbb336c3b70cd3c721b60628657f" + "sha256": "198ee9c3c20cf96cdf2b966f8dcc6e85f22ef4eb7783469559c0f45e7c1ebc6d" }, "pipfile-spec": 6, "requires": {}, @@ -16,19 +16,19 @@ "default": { "amqp": { "hashes": [ - "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2", - "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb" + "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2", + "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359" ], "markers": "python_version >= '3.6'", - "version": "==5.0.6" + "version": "==5.1.1" }, "asgiref": { "hashes": [ - "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9", - "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214" + "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", + "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" ], - "markers": "python_version >= '3.6'", - "version": "==3.4.1" + "markers": "python_version >= '3.7'", + "version": "==3.5.2" }, "asonic": { "hashes": [ @@ -39,19 +39,19 @@ }, "astroid": { "hashes": [ - "sha256:5939cf55de24b92bda00345d4d0659d01b3c7dafb5055165c330bc7c568ba273", - "sha256:776ca0b748b4ad69c00bfe0fff38fa2d21c338e12c84aa9715ee0d473c422778" + "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83", + "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f" ], - "markers": "python_version ~= '3.6'", - "version": "==2.9.0" + "markers": "python_full_version >= '3.7.2'", + "version": "==2.12.12" }, "beautifulsoup4": { "hashes": [ - "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf", - "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891" + "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", + "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" ], "index": "pypi", - "version": "==4.10.0" + "version": "==4.11.1" }, "billiard": { "hashes": [ @@ -61,106 +61,124 @@ "version": "==3.6.4.0" }, "bleach": { + "extras": [ + "css" + ], "hashes": [ - "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", - "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" + "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", + "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" ], - "markers": "python_version >= '3.6'", - "version": "==4.1.0" + "markers": "python_version >= '3.7'", + "version": "==5.0.1" }, "cachetools": { "hashes": [ - "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693", - "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1" + "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757", + "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db" ], - "markers": "python_version ~= '3.5'", - "version": "==4.2.4" + "markers": "python_version ~= '3.7'", + "version": "==5.2.0" }, "celery": { "hashes": [ - "sha256:b41a590b49caf8e6498a57db628e580d5f8dc6febda0f42de5d783aed5b7f808", - "sha256:cc63ea6572d558be65297ba6db7a7979e64c0a3d0d61212d6302ef1ca05a0d22" + "sha256:138420c020cd58d6707e6257b6beda91fd39af7afde5d36c6334d175302c0e14", + "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d" ], "index": "pypi", - "version": "==5.2.1" + "version": "==5.2.7" }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], - "version": "==2021.10.8" + "markers": "python_version >= '3.6'", + "version": "==2022.9.24" }, "cffi": { "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" }, "charset-normalizer": { "hashes": [ - "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", - "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3'", - "version": "==2.0.9" + "markers": "python_version >= '3.6'", + "version": "==2.1.1" }, "click": { "hashes": [ - "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", - "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", + "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" ], - "markers": "python_version >= '3.6'", - "version": "==8.0.3" + "markers": "python_version >= '3.7'", + "version": "==8.1.3" }, "click-didyoumean": { "hashes": [ @@ -186,11 +204,11 @@ }, "colorama": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" ], "index": "pypi", - "version": "==0.4.4" + "version": "==0.4.6" }, "coreapi": { "hashes": [ @@ -208,69 +226,91 @@ }, "cryptography": { "hashes": [ - "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681", - "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed", - "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4", - "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568", - "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e", - "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f", - "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f", - "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712", - "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e", - "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58", - "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44", - "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6", - "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d", - "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636", - "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba", - "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120", - "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3", - "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d", - "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b", - "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81", - "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8" + "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", + "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", + "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", + "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", + "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", + "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", + "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", + "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", + "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", + "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", + "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", + "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", + "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", + "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", + "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", + "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", + "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", + "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", + "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", + "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", + "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", + "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", + "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", + "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", + "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", + "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" ], "markers": "python_version >= '3.6'", - "version": "==36.0.0" + "version": "==38.0.3" + }, + "dill": { + "hashes": [ + "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", + "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" + ], + "markers": "python_version >= '3.7'", + "version": "==0.3.6" }, "django": { "hashes": [ - "sha256:074e8818b4b40acdc2369e67dcd6555d558329785408dcd25340ee98f1f1d5c4", - "sha256:df6f5eb3c797b27c096d61494507b7634526d4ce8d7c8ca1e57a4fb19c0738a3" + "sha256:18ba8efa36b69cfcd4b670d0fa187c6fe7506596f0ababe580e16909bcdec121", + "sha256:3adc285124244724a394fa9b9839cc8cd116faf7d159554c43ecdaa8cdf0b94d" + ], + "index": "pypi", + "version": "==3.2.16" + }, + "django-cors-headers": { + "hashes": [ + "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4", + "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf" ], "index": "pypi", - "version": "==3.2.10" + "version": "==3.13.0" }, "django-elasticsearch-dsl": { "hashes": [ - "sha256:265812e22538899333ebb8622c2b9d053eb4e1e44b24c09730da67c90103a6b3", - "sha256:6a0f96681c033fa04c0a103fab600561f2dd44e7d3b0075723cd05ffc21b716d" + "sha256:3c58a254a6318b169eb904d41d802924b99ea8e53ddc2c596ebba90506cf47fa", + "sha256:811d3909b3387fd55c19d9bbcf0e9a9b234f085df3f8422d59e7519a5f733e0e" ], "index": "pypi", - "version": "==7.2.1" + "version": "==7.2.2" }, "django-filter": { "hashes": [ - "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e", - "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063" + "sha256:ed429e34760127e3520a67f415bec4c905d4649fbe45d0d6da37e6ff5e0287eb", + "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5" ], "index": "pypi", - "version": "==21.1" + "version": "==22.1" }, "django-markdownify": { "hashes": [ - "sha256:2d3e460a34fb4498c8f7a054e7c6d4d5f67cc0792f86331b2af2dc27f776b65c", - "sha256:b060eb7869f493f7bff390ba2f7a81b981dd6c5f163dbb95423b6cf714ca4d23" + "sha256:1bc2b7e1c51c696b7c460eacb71d7fd94dadf6575897feae7ba49e155502a3ba", + "sha256:6906d75197e9fc1c5faf1b68241e4abbf0dd00a627e75bfce6ee7e54b46d737f" ], "index": "pypi", - "version": "==0.9.0" + "version": "==0.9.2" }, "django-model-utils": { "hashes": [ - "sha256:a768a25c80514e0ad4e4a6f9c02c44498985f36c5dfdea47b5b1e8cf994beba6", - "sha256:e7a95e102f9c9653427eadab980d5d59e1dea972913b9c9e01ac37f86bba0ddf" + "sha256:2e2e4f13e4f14613134a9777db7ad4265f59a1d8f1384107bcaa3028fe3c87c1", + "sha256:8c0b0177bab909a8635b602d960daa67e80607aa5469217857271a60726d7a4b" ], - "version": "==4.2.0" + "markers": "python_version >= '3.7'", + "version": "==4.3.1" }, "django-multiselectfield": { "hashes": [ @@ -282,35 +322,35 @@ }, "django-notifications-hq": { "hashes": [ - "sha256:debeb71b7076b08487b40bf07664d1cc43b9977c4480bbc969b30236dda7a461", - "sha256:dfc6f8bd4034ceae91143bc3802ddfb6e276eaec90e63dd23e2584c052561576" + "sha256:1ff90fbddb0a9dba3fdc57758e799b755c8844f673cd92e9ffaa64cedd33d5f5", + "sha256:5540a99681c0e73399726d662ab74f23c45ba385036431ecd75ffc962f02c793" ], "index": "pypi", - "version": "==1.6.0" + "version": "==1.7.0" }, "djangorestframework": { "hashes": [ - "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf", - "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2" + "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", + "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08" ], "index": "pypi", - "version": "==3.12.4" + "version": "==3.14.0" }, "drf-yasg": { "hashes": [ - "sha256:8b72e5b1875931a8d11af407be3a9a5ba8776541492947a0df5bafda6b7f8267", - "sha256:d50f197c7f02545d0b736df88c6d5cf874f8fea2507ad85ad7de6ae5bf2d9e5a" + "sha256:4a156d195fdccc51b40a227955588d982ca43c2e327927c7713bf967f5589913", + "sha256:887c9f79e64f46aa48974234e61029b1bea6b12ea628a8fc8a3697589add1d3e" ], "index": "pypi", - "version": "==1.20.0" + "version": "==1.21.4" }, "elasticsearch": { "hashes": [ - "sha256:436f871848a5020bf9b47495812b229b59bd0c5d7e40adbd5e3c89896b311704", - "sha256:83c299a08fc8737c72454e6d3b2a01ba1b194e4f4d9e4f8bae7058cec326f39f" + "sha256:555170b4e13a823f4472bc12a148aef90febd5b90b16be83651d35524f34acb3", + "sha256:ed9c0cd58e05959a56e306ecf444f794da6afde75b213e26758f7a317e5e668c" ], "index": "pypi", - "version": "==7.15.2" + "version": "==7.17.7" }, "elasticsearch-dsl": { "hashes": [ @@ -322,51 +362,51 @@ }, "feedparser": { "hashes": [ - "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a", - "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661" + "sha256:27da485f4637ce7163cdeab13a80312b93b7d0c1b775bef4a47629a3110bca51", + "sha256:79c257d526d13b944e965f6095700587f27388e50ea16fd245babe4dfae7024f" ], "index": "pypi", - "version": "==6.0.8" + "version": "==6.0.10" }, "flake8": { "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" + "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", + "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" ], "index": "pypi", - "version": "==4.0.1" + "version": "==5.0.4" }, "freezegun": { "hashes": [ - "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3", - "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712" + "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446", + "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f" ], "index": "pypi", - "version": "==1.1.0" + "version": "==1.2.2" }, "google-api-core": { "hashes": [ - "sha256:97349cc18c2bb2415f64f1353a80273a289a61294ce3eb2f7ce682d251bdd997", - "sha256:e7853735d4f51f4212d6bf9750620d76fc0106c0f271be0c3f43b73501c7ddf9" + "sha256:10c06f7739fe57781f87523375e8e1a3a4674bf6392cd6131a3222182b971320", + "sha256:34f24bd1d5f72a8c4519773d99ca6bf080a6c4e041b4e9f024fe230191dda62e" ], - "markers": "python_version >= '3.6'", - "version": "==2.2.2" + "markers": "python_version >= '3.7'", + "version": "==2.10.2" }, "google-api-python-client": { "hashes": [ - "sha256:1449c6941afabc32a274be54d17f262615e3a970114c386b62986e29618623f3", - "sha256:619fe50155e73342c17aba4bbb2a08be8ce6ae00b795af383de7d6616b485c94" + "sha256:2c6611530308b3f931dcf1360713aa3a20cf465d0bf2bac65f2ec99e8c9860de", + "sha256:b8a0ca8454ad57bc65199044717d3d214197ae1e2d666426bbcd4021b36762e0" ], "index": "pypi", - "version": "==2.32.0" + "version": "==2.65.0" }, "google-auth": { "hashes": [ - "sha256:a348a50b027679cb7dae98043ac8dbcc1d7951f06d8387496071a1e05a2465c0", - "sha256:d83570a664c10b97a1dc6f8df87e5fdfff012f48f62be131e449c20dfc32630e" + "sha256:ccaa901f31ad5cbb562615eb8b664b3dd0bf5404a67618e642307f00613eda4d", + "sha256:f5d8701633bebc12e0deea4df8abd8aff31c28b355360597f7f2ee60f2e4d016" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.3.3" + "version": "==2.14.1" }, "google-auth-httplib2": { "hashes": [ @@ -377,27 +417,27 @@ }, "google-auth-oauthlib": { "hashes": [ - "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73", - "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a" + "sha256:860e54c4b58b2664116c9cb44325bc0ec92bcd93e8211698ceea911b1b873b86", + "sha256:9940f543f77d1447432a93781d7c931fb53e418023351ad4bf9e92837a1154ec" ], "markers": "python_version >= '3.6'", - "version": "==0.4.6" + "version": "==0.7.1" }, "googleapis-common-protos": { "hashes": [ - "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4", - "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0" + "sha256:27a849d6205838fb6cc3c1c21cb9800707a661bb21c6ce7fb13e99eb1f8a0c46", + "sha256:a9f4a1d7f6d9809657b7f1316a1aa527f6664891531bcfcc13b6696e685f443c" ], - "markers": "python_version >= '3.6'", - "version": "==1.53.0" + "markers": "python_version >= '3.7'", + "version": "==1.57.0" }, "gspread": { "hashes": [ - "sha256:55dd9e257ad45c479aed9283e5abe8d517a0c4e2dd443bf0a9849b53f826c0ca", - "sha256:5681d60d5951935df1ecf5ce553785fc6a5227bbcabd894de28a724f3e877ba4" + "sha256:41f7a416425f1ec5a1b677f49b8fbf599102766c27ed7be6601a58c9a1550ebc", + "sha256:d3bbff4b7aad0fc2c986458e148537a02fe7b46e7162f41f3a42392bfa2adb89" ], "index": "pypi", - "version": "==5.0.0" + "version": "==5.6.2" }, "html5lib": { "hashes": [ @@ -415,27 +455,27 @@ }, "httplib2": { "hashes": [ - "sha256:6b937120e7d786482881b44b8eec230c1ee1c5c1d06bce8cc865f25abbbf713b", - "sha256:e404681d2fbcec7506bcb52c503f2b021e95bee0ef7d01e5c221468a2406d8dc" + "sha256:987c8bb3eb82d3fa60c68699510a692aa2ad9c4bd4f123e51dfb1488c14cdd01", + "sha256:fc144f091c7286b82bec71bdbd9b27323ba709cc612568d3000893bfd9cb4b34" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.20.2" + "version": "==0.21.0" }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", - "version": "==3.3" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "importlib-metadata": { "hashes": [ - "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100", - "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb" + "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", + "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" ], "index": "pypi", - "version": "==4.8.2" + "version": "==5.0.0" }, "inflection": { "hashes": [ @@ -462,11 +502,11 @@ }, "jinja2": { "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" + "markers": "python_version >= '3.7'", + "version": "==3.1.2" }, "jsonfield": { "hashes": [ @@ -478,129 +518,98 @@ }, "kombu": { "hashes": [ - "sha256:0f5d0763fb916808f617b886697b2be28e6bc35026f08e679697fc814b48a608", - "sha256:d36f0cde6a18d9eb7b6b3aa62a59bfdff7f5724689850e447eca5be8efc9d501" + "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610", + "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4" ], "markers": "python_version >= '3.7'", - "version": "==5.2.2" + "version": "==5.2.4" }, "lazy-object-proxy": { "hashes": [ - "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653", - "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61", - "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2", - "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837", - "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3", - "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43", - "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726", - "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3", - "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587", - "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8", - "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a", - "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd", - "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f", - "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad", - "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4", - "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b", - "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf", - "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981", - "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741", - "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e", - "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", - "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" + "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada", + "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d", + "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7", + "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe", + "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd", + "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c", + "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858", + "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288", + "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec", + "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f", + "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891", + "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c", + "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25", + "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156", + "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8", + "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f", + "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e", + "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0", + "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.6.0" + "markers": "python_version >= '3.7'", + "version": "==1.8.0" }, "markdown": { "hashes": [ - "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006", - "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3" + "sha256:08fb8465cffd03d10b9dd34a5c3fea908e20391a2a90b88d66362cb05beed186", + "sha256:3b809086bb6efad416156e00a0da66fe47618a5d6918dd688f53f40c8e4cfeff" ], "index": "pypi", - "version": "==3.3.6" + "version": "==3.4.1" }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", + "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", + "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", + "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", + "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", + "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", + "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", + "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", + "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", + "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", + "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", + "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", + "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", + "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", + "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", + "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", + "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", + "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", + "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", + "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", + "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", + "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", + "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", + "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", + "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", + "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", + "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", + "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", + "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", + "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", + "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", + "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", + "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", + "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", + "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", + "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", + "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", + "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", + "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", + "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.7'", + "version": "==2.1.1" }, "mccabe": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "version": "==0.6.1" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, "oauth2client": { "hashes": [ @@ -612,11 +621,11 @@ }, "oauthlib": { "hashes": [ - "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc", - "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3" + "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", + "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" ], "markers": "python_version >= '3.6'", - "version": "==3.1.1" + "version": "==3.2.2" }, "packaging": { "hashes": [ @@ -628,96 +637,106 @@ }, "pillow": { "hashes": [ - "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76", - "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585", - "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b", - "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8", - "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55", - "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc", - "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645", - "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff", - "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc", - "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b", - "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6", - "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20", - "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e", - "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a", - "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779", - "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02", - "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39", - "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f", - "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a", - "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409", - "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c", - "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488", - "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b", - "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d", - "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09", - "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b", - "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153", - "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9", - "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad", - "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df", - "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df", - "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed", - "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed", - "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698", - "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29", - "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649", - "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49", - "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b", - "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2", - "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a", - "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78" + "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040", + "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8", + "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65", + "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2", + "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627", + "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07", + "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef", + "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535", + "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c", + "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc", + "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3", + "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1", + "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c", + "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa", + "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32", + "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502", + "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4", + "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f", + "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812", + "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636", + "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20", + "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c", + "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91", + "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe", + "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b", + "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad", + "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9", + "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72", + "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4", + "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de", + "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29", + "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee", + "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c", + "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7", + "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11", + "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c", + "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c", + "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448", + "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b", + "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20", + "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228", + "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd", + "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699", + "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b", + "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2", + "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4", + "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c", + "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f", + "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2", + "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c", + "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3", + "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193", + "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48", + "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02", + "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8", + "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e", + "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f", + "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b", + "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74", + "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb", + "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0" ], "index": "pypi", - "version": "==8.4.0" + "version": "==9.3.0" }, "platformdirs": { "hashes": [ - "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", - "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7", + "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10" ], - "markers": "python_version >= '3.6'", - "version": "==2.4.0" + "markers": "python_version >= '3.7'", + "version": "==2.5.4" }, "prompt-toolkit": { "hashes": [ - "sha256:5f29d62cb7a0ecacfa3d8ceea05a63cd22500543472d64298fc06ddda906b25d", - "sha256:7053aba00895473cb357819358ef33f11aa97e4ac83d38efb123e5649ceeecaf" + "sha256:24becda58d49ceac4dc26232eb179ef2b21f133fecda7eed6018d341766ed76e", + "sha256:e7f2129cba4ff3b3656bbdda0e74ee00d2f874a8bcdb9dd16f5fec7b3e173cae" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.23" + "version": "==3.0.32" }, "protobuf": { "hashes": [ - "sha256:038daf4fa38a7e818dd61f51f22588d61755160a98db087a046f80d66b855942", - "sha256:28ccea56d4dc38d35cd70c43c2da2f40ac0be0a355ef882242e8586c6d66666f", - "sha256:36d90676d6f426718463fe382ec6274909337ca6319d375eebd2044e6c6ac560", - "sha256:3cd0458870ea7d1c58e948ac8078f6ba8a7ecc44a57e03032ed066c5bb318089", - "sha256:5935c8ce02e3d89c7900140a8a42b35bc037ec07a6aeb61cc108be8d3c9438a6", - "sha256:615b426a177780ce381ecd212edc1e0f70db8557ed72560b82096bd36b01bc04", - "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7", - "sha256:655264ed0d0efe47a523e2255fc1106a22f6faab7cc46cfe99b5bae085c2a13e", - "sha256:6e8ea9173403219239cdfd8d946ed101f2ab6ecc025b0fda0c6c713c35c9981d", - "sha256:71b0250b0cfb738442d60cab68abc166de43411f2a4f791d31378590bfb71bd7", - "sha256:74f33edeb4f3b7ed13d567881da8e5a92a72b36495d57d696c2ea1ae0cfee80c", - "sha256:77d2fadcf369b3f22859ab25bd12bb8e98fb11e05d9ff9b7cd45b711c719c002", - "sha256:8b30a7de128c46b5ecb343917d9fa737612a6e8280f440874e5cc2ba0d79b8f6", - "sha256:8e51561d72efd5bd5c91490af1f13e32bcba8dab4643761eb7de3ce18e64a853", - "sha256:a529e7df52204565bcd33738a7a5f288f3d2d37d86caa5d78c458fa5fabbd54d", - "sha256:b691d996c6d0984947c4cf8b7ae2fe372d99b32821d0584f0b90277aa36982d3", - "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8", - "sha256:d83e1ef8cb74009bebee3e61cc84b1c9cd04935b72bca0cbc83217d140424995", - "sha256:d8919368410110633717c406ab5c97e8df5ce93020cfcf3012834f28b1fab1ea", - "sha256:db3532d9f7a6ebbe2392041350437953b6d7a792de10e629c1e4f5a6b1fe1ac6", - "sha256:e7b24c11df36ee8e0c085e5b0dc560289e4b58804746fb487287dda51410f1e2", - "sha256:e7e8d2c20921f8da0dea277dfefc6abac05903ceac8e72839b2da519db69206b", - "sha256:e813b1c9006b6399308e917ac5d298f345d95bb31f46f02b60cd92970a9afa17", - "sha256:fd390367fc211cc0ffcf3a9e149dfeca78fecc62adb911371db0cec5c8b7472d" + "sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719", + "sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b", + "sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce", + "sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99", + "sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392", + "sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b", + "sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965", + "sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444", + "sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c", + "sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536", + "sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf", + "sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1", + "sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740", + "sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa" ], - "markers": "python_version >= '3.5'", - "version": "==3.19.1" + "markers": "python_version >= '3.7'", + "version": "==4.21.9" }, "py-vapid": { "hashes": [ @@ -763,11 +782,11 @@ }, "pycodestyle": { "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" + "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", + "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.8.0" + "markers": "python_version >= '3.6'", + "version": "==2.9.1" }, "pycparser": { "hashes": [ @@ -787,34 +806,35 @@ }, "pyflakes": { "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" + "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", + "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.0" + "markers": "python_version >= '3.6'", + "version": "==2.5.0" }, "pylint": { "hashes": [ - "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9", - "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74" + "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df", + "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004" ], "index": "pypi", - "version": "==2.12.2" + "version": "==2.15.5" }, "pylint-django": { "hashes": [ - "sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b", - "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc" + "sha256:0ac090d106c62fe33782a1d01bda1610b761bb1c9bf5035ced9d5f23a13d8591", + "sha256:56b12b6adf56d548412445bd35483034394a1a94901c3f8571980a13882299d5" ], "index": "pypi", - "version": "==2.4.4" + "version": "==2.5.3" }, "pylint-plugin-utils": { "hashes": [ - "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", - "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a" + "sha256:b3d43e85ab74c4f48bb46ae4ce771e39c3a20f8b3d56982ab17aa73b4f98d535", + "sha256:ce48bc0516ae9415dd5c752c940dfe601b18fe0f48aa249f2386adfa95a004dd" ], - "version": "==0.6" + "markers": "python_full_version >= '3.6.2'", + "version": "==0.7" }, "pylint-runner": { "hashes": [ @@ -826,19 +846,19 @@ }, "pyotp": { "hashes": [ - "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28", - "sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432" + "sha256:2e746de4f15685878df6d022c5691627af9941eec18e0d513f05497f5fa7711f", + "sha256:ce989faba0df77dc032b45e51c6cca42bcf20896c8d3d1e7cd759a53dc7d6cb5" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.0" }, "pyparsing": { "hashes": [ - "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", - "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.6" + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" }, "python-dateutil": { "hashes": [ @@ -850,10 +870,10 @@ }, "pytz": { "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", + "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" ], - "version": "==2021.3" + "version": "==2022.6" }, "pywebpush": { "hashes": [ @@ -864,6 +884,7 @@ }, "pyyaml": { "hashes": [ + "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", @@ -875,26 +896,32 @@ "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" ], @@ -903,70 +930,74 @@ }, "requests": { "hashes": [ - "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", - "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.26.0" + "version": "==2.28.1" }, "requests-oauthlib": { "hashes": [ - "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", - "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" + "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", + "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" ], - "version": "==1.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.3.1" }, "rsa": { "hashes": [ - "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", - "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" + "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", + "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" ], "markers": "python_version >= '3.6' and python_version < '4'", - "version": "==4.8" + "version": "==4.9" }, "ruamel.yaml": { "hashes": [ - "sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be", - "sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f" + "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7", + "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af" ], "markers": "python_version >= '3'", - "version": "==0.17.17" + "version": "==0.17.21" }, "ruamel.yaml.clib": { "hashes": [ - "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd", - "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0", - "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277", - "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104", - "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd", - "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78", - "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99", - "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527", - "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84", - "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7", - "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468", - "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b", - "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94", - "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233", - "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb", - "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5", - "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe", - "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751", - "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502", - "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed", - "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c" - ], - "markers": "python_version < '3.10' and platform_python_implementation == 'CPython'", - "version": "==0.2.6" - }, - "setuptools": { - "hashes": [ - "sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf", - "sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0" - ], - "markers": "python_version >= '3.6'", - "version": "==59.5.0" + "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e", + "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3", + "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5", + "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497", + "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f", + "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac", + "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697", + "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763", + "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282", + "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1", + "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072", + "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9", + "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5", + "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231", + "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93", + "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b", + "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb", + "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f", + "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307", + "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8", + "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b", + "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b", + "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640", + "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7", + "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a", + "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71", + "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8", + "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7", + "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80", + "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e", + "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab", + "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0", + "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646" + ], + "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", + "version": "==0.2.7" }, "sgmllib3k": { "hashes": [ @@ -984,19 +1015,19 @@ }, "soupsieve": { "hashes": [ - "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb", - "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9" + "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", + "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" ], "markers": "python_version >= '3.6'", - "version": "==2.3.1" + "version": "==2.3.2.post1" }, "sqlparse": { "hashes": [ - "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", - "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" + "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34", + "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268" ], "markers": "python_version >= '3.5'", - "version": "==0.4.2" + "version": "==0.4.3" }, "swapper": { "hashes": [ @@ -1005,21 +1036,28 @@ ], "version": "==1.3.0" }, - "toml": { + "tinycss2": { + "hashes": [ + "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf", + "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8" + ], + "version": "==1.1.1" + }, + "tomli": { "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" + "markers": "python_version < '3.11'", + "version": "==2.0.1" }, - "typing-extensions": { + "tomlkit": { "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b", + "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73" ], - "markers": "python_version < '3.10'", - "version": "==4.0.1" + "markers": "python_version >= '3.6'", + "version": "==0.11.6" }, "uritemplate": { "hashes": [ @@ -1031,11 +1069,11 @@ }, "urllib3": { "hashes": [ - "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", - "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.7" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" }, "vine": { "hashes": [ @@ -1061,69 +1099,107 @@ }, "wrapt": { "hashes": [ - "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179", - "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096", - "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374", - "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df", - "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185", - "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785", - "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7", - "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909", - "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918", - "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33", - "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068", - "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829", - "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af", - "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79", - "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce", - "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc", - "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36", - "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade", - "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca", - "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32", - "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125", - "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e", - "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709", - "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f", - "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b", - "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb", - "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb", - "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489", - "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640", - "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb", - "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851", - "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d", - "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44", - "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13", - "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2", - "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb", - "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b", - "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9", - "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755", - "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c", - "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a", - "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf", - "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3", - "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229", - "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e", - "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de", - "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554", - "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10", - "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80", - "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", - "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.13.3" + "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", + "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", + "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", + "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", + "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", + "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", + "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", + "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", + "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", + "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", + "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", + "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", + "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", + "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", + "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", + "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", + "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", + "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", + "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", + "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", + "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", + "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", + "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", + "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", + "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", + "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", + "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", + "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", + "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", + "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", + "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", + "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", + "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", + "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", + "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", + "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", + "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", + "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", + "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", + "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", + "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", + "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", + "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", + "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", + "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", + "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", + "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", + "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", + "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", + "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", + "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", + "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", + "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", + "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", + "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", + "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", + "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", + "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", + "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", + "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", + "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", + "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", + "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", + "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" + ], + "markers": "python_version < '3.11'", + "version": "==1.14.1" }, "zipp": { "hashes": [ - "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832", - "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc" + "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", + "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" ], - "markers": "python_version >= '3.6'", - "version": "==3.6.0" + "markers": "python_version >= '3.7'", + "version": "==3.10.0" } }, - "develop": {} + "develop": { + "autopep8": { + "hashes": [ + "sha256:8b1659c7f003e693199f52caffdc06585bb0716900bbc6a7442fd931d658c077", + "sha256:ad924b42c2e27a1ac58e432166cc4588f5b80747de02d0d35b1ecbd3e7d57207" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", + "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" + ], + "markers": "python_version >= '3.6'", + "version": "==2.9.1" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + } + } } diff --git a/README.md b/README.md index b38cde41..d8b6539e 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,60 @@ # InstiApp + API in Django for InstiApp, the one platform for all student activities at Indian Institute of Technology, Bombay! InstiApp's features include upcoming events, placement blog, news and general information on every active club/body in the Institute. [![InstiApp](https://insti.app/instiapp-badge-gh.svg)](https://insti.app) [![Github Actions](https://github.com/wncc/instiapp-api/workflows/Github%20Actions/badge.svg)](https://github.com/wncc/instiapp-api/actions) [![codecov](https://codecov.io/gh/wncc/instiapp-api/branch/master/graph/badge.svg)](https://codecov.io/gh/wncc/instiapp-api) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7e6a386dbec649c99aa6a10218cc3768)](https://www.codacy.com/manual/pulsejet/instiapp-api?utm_source=github.com&utm_medium=referral&utm_content=wncc/instiapp-api&utm_campaign=Badge_Grade) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7e6a386dbec649c99aa6a10218cc3768)](https://www.codacy.com/manual/pulsejet/instiapp-api?utm_source=github.com&utm_medium=referral&utm_content=wncc/instiapp-api&utm_campaign=Badge_Grade) [![Requirements Status](https://requires.io/github/wncc/instiapp-api/requirements.svg?branch=master)](https://requires.io/github/wncc/instiapp-api/requirements/?branch=master) [![GitHub license](https://img.shields.io/github/license/wncc/instiapp-api.svg)](https://github.com/wncc/instiapp-api/blob/master/LICENSE) ## Setup + To setup dependenices, install `pipenv` and run `pipenv sync`. You might want to run `export PIPENV_VENV_IN_PROJECT=true` first to create the `virtualenv` in the project folder. You can then activate the virtual environment with `pipenv shell`. After getting in the virtual environment run `pipenv sync` to install all the depencdencies in new env. -* `python manage.py migrate` to create a new database. -* `python manage.py createsuperuser` will let you create a new user to use the admin panel for testing. -* `python manage.py runserver` to start a local server. -* `flake8` to lint with `flake8`. -* `pylint_runner` to check for code style and other errors statically with `pylint` in all files. +- `python manage.py migrate` to create a new database. +- `python manage.py createsuperuser` will let you create a new user to use the admin panel for testing. +- `python manage.py runserver` to start a local server. +- `flake8` to lint with `flake8`. +- `pylint_runner` to check for code style and other errors statically with `pylint` in all files. It is recommended to set up your IDE with both `pylint` and `flake8`, since these will cause the CircleCI build to fail. Google's [Python Style Guide](https://google.github.io/styleguide/pyguide.html) is followed upto a certain extent in all modules. ## Running Tests + Tests can be run in two configurations: + ### Without Celery + This is the recommended and default configuration, and should suffice for all developmental purposes except if you are working with async tasks or notifications. Simply use `python manage.py test --settings backend.settings_test` to run automated tests. + ### With Celery + This is the default configuration for `full-test` GitHub builds. To test under this configuration, start a local PostgresQL and RabbitMQ server, and an instance of celery in background with `celery -A backend worker --pool=solo -l info`. Once celery is processing background tasks, you can run tests as `python manage.py test --settings backend.settings_test --keepdb`, ensuring that the database `test_instiapp` is created in postgres beforehand. The following environment variables must be set: -* `DJANGO_SETTINGS_MODULE` to `backend.settings_test` -* `NO_CELERY` to `false` + +- `DJANGO_SETTINGS_MODULE` to `backend.settings_test` +- `NO_CELERY` to `false` + ### FTS + Full-Text Search is implemented with [Sonic](https://github.com/valeriansaliou/sonic). To set up, install `cargo` and `sonic` and start it on `localhost`. Then set the `USE_SONIC` setting to `True`. ## Documentation -Autogenerated OpenAPI specification can be accessed at `http://server/api/docs/` (or at the [official deployment](https://insti.app/api/docs/)) + +Autogenerated OpenAPI specification can be accessed at `http://server/api/docs/` (or at the [official deployment](https://gymkhana.iitb.ac.in/instiapp/api/docs/)) ## Contributing + Pull requests are welcome, but make sure the following criteria are satisfied -* If you are (possibly) breaking an existing feature, state this explicitly in the PR description -* Commit messages should be in present tense, descriptive and relevant, closely following the [GNOME Commit Message Guidelines](https://wiki.gnome.org/Git/CommitMessages). Adding a tag to the message is optional (for now). Commits should not have git tags unless they indicate a version change. -* Documentation should be updated when the API is modified -* All required status checks must pass. Barring exceptional cases, relevant tests should be added/updated whenever necessary. -* Barring exceptional cases, Codacy should not report any new issues -* Follow the general style of the project. Badly written or undocumented code might be rejected -* If you are proposing a new model or modifications to an existing one, create an issue first, explaining why it is useful -* Outdated, unsupported or closed-source libraries should not be used -* Be nice! + +- If you are (possibly) breaking an existing feature, state this explicitly in the PR description +- Commit messages should be in present tense, descriptive and relevant, closely following the [GNOME Commit Message Guidelines](https://wiki.gnome.org/Git/CommitMessages). Adding a tag to the message is optional (for now). Commits should not have git tags unless they indicate a version change. +- Documentation should be updated when the API is modified +- All required status checks must pass. Barring exceptional cases, relevant tests should be added/updated whenever necessary. +- Barring exceptional cases, Codacy should not report any new issues +- Follow the general style of the project. Badly written or undocumented code might be rejected +- If you are proposing a new model or modifications to an existing one, create an issue first, explaining why it is useful +- Outdated, unsupported or closed-source libraries should not be used +- Be nice! diff --git a/backend/settings.py b/backend/settings.py index f17723e3..5c7089d1 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -17,8 +17,8 @@ # SSO Config SSO_TOKEN_URL = 'https://gymkhana.iitb.ac.in/sso/oauth/token/' SSO_PROFILE_URL = 'https://gymkhana.iitb.ac.in/sso/user/api/user/?fields=first_name,last_name,type,profile_picture,sex,username,email,program,contacts,insti_address,secondary_emails,mobile,roll_number' -SSO_CLIENT_ID = 'gxzx6u7aw6wco6SJBioORPwum5Sug7OrIPrm8r2W' -SSO_CLIENT_ID_SECRET_BASE64 = 'Z3h6eDZ1N2F3NndjbzZTSkJpb09SUHd1bTVTdWc3T3JJUHJtOHIyVzpVTjc2UW9PMzVpS0FGcm5wa3dzdmJIUjQxeXh5QTVvVkcxNjV2cm1ZeWpNcGk2YWFzRzZhSXJDSTlvWk9odTFaQzlVN1BFaE9nZ3c0WmpmazF5bURuWlhEalVGb2ViV2FaRjh6R2E3QUI5S3J3TEJrNHFKTHpadE9DUVZvUTlLVQ==' +SSO_CLIENT_ID = '5jyMJufq0Vk0aDlj9Hnudsj84UfbFZlYRUnn02Xd' +SSO_CLIENT_ID_SECRET_BASE64 = 'NWp5TUp1ZnEwVmswYURsajlIbnVkc2o4NFVmYkZabFlSVW5uMDJYZDo2RTN5S3g4OU5XWVFxcWFJaUdwcE9RdFF4TmZxSkxFYXhkcDVXb2s2VzZsdGxDQWpPWmM3SWJrVXVpUXdnaTZNaGVoUU1LWnVTelFUMU1EOHBockhhVThvVUk5S1hYZ2NHMG51UE03Wkk3V0xoWjYydG5OZFU4Uk02TTJXaXdJZw==' # Password Login SSO_DEFAULT_REDIR = 'https://insti.app/login' @@ -31,6 +31,7 @@ VAPID_PRIV_KEY = "" FCM_SERVER_KEY = "" +MESSI_ACCESS_TOKEN = "" # Change this to LOGGING to enable SQLite logging NO_LOGGING = { diff --git a/backend/settings_base.py b/backend/settings_base.py index 42e50282..fccaa893 100644 --- a/backend/settings_base.py +++ b/backend/settings_base.py @@ -11,9 +11,10 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_URL_PATH = '' # Elasticsearch configuration -ELASTICSEARCH_DSL={ +ELASTICSEARCH_DSL = { 'default': { 'hosts': 'localhost:9200' }, @@ -33,6 +34,7 @@ 'rest_framework', 'drf_yasg', 'django_elasticsearch_dsl', + 'corsheaders', 'achievements.apps.AchievementsConfig', 'events.apps.EventsConfig', @@ -49,6 +51,8 @@ 'querybot.apps.QuerybotConfig', 'external.apps.ExternalConfig', 'alumni.apps.AlumniConfig', + 'community.apps.CommunityConfig', + 'notifications', 'markdownify', ] @@ -57,7 +61,8 @@ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - #'django.middleware.csrf.CsrfViewMiddleware', + 'corsheaders.middleware.CorsMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', 'backend.middle.DisableCSRFMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -156,10 +161,17 @@ LOGO_URL = 'https://insti.app/assets/logo.png' # Placement blog URLs -PLACEMENTS_URL = 'https://campus.placements.iitb.ac.in/blog/placement?feed=rss2' -TRAINING_BLOG_URL = 'https://campus.placements.iitb.ac.in/blog/internship?feed=rss2' +PLACEMENTS_URL = 'https://campus.placements.iitb.ac.in/blog/placement/authplacement/?feed=rss2' +TRAINING_BLOG_URL = 'https://campus.placements.iitb.ac.in/blog/internship/authinternship/?feed=rss2' + +PLACEMENTS_URL_VAL = 'https://campus.placements.iitb.ac.in/blog/placement/?feed=rss2' +TRAINING_BLOG_URL_VAL = 'https://campus.placements.iitb.ac.in/blog/internship/?feed=rss2' + + EXTERNAL_BLOG_URL = 'https://gymkhana.iitb.ac.in/externalblog' +MESSI_BASE_URL = "https://instamess.gymkhana.iitb.ac.in" + # Names of bodies to notify when there are new posts on placement/training blog PLACEMENTS_BLOG_BODY = 'Placement Blog' TRAINING_BLOG_BODY = 'Internship Blog' @@ -167,10 +179,13 @@ # Authentication for chores LDAP_USERNAME = None LDAP_PASSWORD = None +SSO_TOTP_TOKEN = None +CHATBOT_LOG = "chatbot_logs.json" -if 'LDAP_USERNAME' in os.environ and 'LDAP_PASSWORD' in os.environ: +if 'LDAP_USERNAME' in os.environ and 'LDAP_PASSWORD' in os.environ and 'SSO_TOTP_TOKEN' in os.environ: LDAP_USERNAME = os.environ['LDAP_USERNAME'] LDAP_PASSWORD = os.environ['LDAP_PASSWORD'] + SSO_TOTP_TOKEN = os.environ['SSO_TOTP_TOKEN'] print('INFO: LDAP username and password present in environment.') # Flip for broken external server certificates @@ -196,7 +211,7 @@ COMPLAINT_AUTO_SUBSCRIBE = True -DEFAULT_AUTO_FIELD='django.db.models.AutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # true when elasticsearch is configured USE_ELASTIC = True diff --git a/backend/settings_test.py b/backend/settings_test.py index f0b8ba68..05f1be67 100644 --- a/backend/settings_test.py +++ b/backend/settings_test.py @@ -33,3 +33,6 @@ PLACEMENTS_URL = 'http://localhost:33000/placementblog' TRAINING_BLOG_URL = 'http://localhost:33000/trainingblog' EXTERNAL_BLOG_URL = 'http://localhost:33000/externalblog' + +PLACEMENTS_URL_VAL = 'http://localhost:33000/placementblog' +TRAINING_BLOG_URL_VAL = 'http://localhost:33000/trainingblog' diff --git a/backend/urls.py b/backend/urls.py index 81e4008e..d7a38272 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -15,6 +15,7 @@ """ from django.contrib import admin from django.urls import path, include +from django.conf import settings from django.contrib.sitemaps.views import sitemap from rest_framework import permissions from drf_yasg.views import get_schema_view @@ -39,14 +40,14 @@ def api_base(prefix=None): default_version='v1', description="InstiApp IIT Bombay API", terms_of_service="https://insti.app/tos.html", - contact=openapi.Contact(email="support@insti.app"), + contact=openapi.Contact(email="devcom@iitb.ac.in"), license=openapi.License(name="AGPLv3"), ), public=True, permission_classes=(permissions.AllowAny,), ) -urlpatterns = [ +base_urlpatterns = [ # Admin site path('admin/', admin.site.urls), @@ -65,6 +66,7 @@ def api_base(prefix=None): path(api_base(), include('other.urls')), path(api_base(), include('querybot.urls')), path(api_base(), include('external.urls')), + path(api_base(), include('community.urls')), path(api_base('venter'), include("venter.urls")), # Non-API @@ -74,3 +76,7 @@ def api_base(prefix=None): 'sitemaps': sitemaps() }, name='django.contrib.sitemaps.views.sitemap') ] + +urlpatterns = [ + path(settings.BASE_URL_PATH, include(base_urlpatterns)), +] diff --git a/bodies/admin.py b/bodies/admin.py index 45214352..ec4fc2ea 100644 --- a/bodies/admin.py +++ b/bodies/admin.py @@ -1,5 +1,11 @@ from django.contrib import admin from bodies.models import Body, BodyChildRelation -admin.site.register(Body) +class BodyAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ['name'] + ordering = ('-time_of_creation',) + + +admin.site.register(Body, BodyAdmin) admin.site.register(BodyChildRelation) diff --git a/community/__init__.py b/community/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/admin.py b/community/admin.py new file mode 100644 index 00000000..7d76f4d8 --- /dev/null +++ b/community/admin.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from community.models import Community, CommunityPost, CommunityPostUserReaction + +class CommunityPostAdmin(admin.ModelAdmin): + search_fields = ['content', 'community__name', 'posted_by__name'] + list_display = ('content', 'community', 'thread_rank', 'posted_by') + list_filter = ('community__name', 'thread_rank') + raw_id_fields = ('reported_by', 'posted_by', 'community', 'parent', 'reacted_by', + 'followed_by', 'interests', 'tag_user', 'tag_body', 'tag_location') + + +admin.site.register(Community) +admin.site.register(CommunityPost, CommunityPostAdmin) +admin.site.register(CommunityPostUserReaction) diff --git a/community/apps.py b/community/apps.py new file mode 100644 index 00000000..efff575c --- /dev/null +++ b/community/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig + +class CommunityConfig(AppConfig): + name = 'community' diff --git a/community/migrations/0001_initial.py b/community/migrations/0001_initial.py new file mode 100644 index 00000000..6eea6a0b --- /dev/null +++ b/community/migrations/0001_initial.py @@ -0,0 +1,99 @@ +# Generated by Django 3.2.13 on 2022-06-30 11:30 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('bodies', '0023_body_canonical_name'), + ('users', '0038_auto_20210606_2237'), + ('achievements', '0012_auto_20211201_1642'), + ('locations', '0014_location_str_id'), + ] + + operations = [ + migrations.CreateModel( + name='Community', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('str_id', models.CharField(editable=False, max_length=58, null=True)), + ('name', models.CharField(max_length=100)), + ('about', models.TextField()), + ('description', models.TextField()), + ('logo_image', models.URLField(blank=True, null=True)), + ('cover_image', models.URLField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('followers_count', models.IntegerField(default=0)), + ], + options={ + 'verbose_name': 'Community', + 'verbose_name_plural': 'Communities', + 'ordering': ('-created_at',), + }, + ), + migrations.CreateModel( + name='CommunityPost', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('str_id', models.CharField(editable=False, max_length=58, null=True)), + ('time_of_creation', models.DateTimeField(auto_now_add=True)), + ('time_of_modification', models.DateTimeField(auto_now=True)), + ('content', models.TextField(blank=True)), + ('image_url', models.TextField(blank=True, null=True)), + ('view_count', models.IntegerField(default=0)), + ('threadRank', models.IntegerField(default=1)), + ('status', models.IntegerField()), + ('comments', models.ManyToManyField(blank=True, related_name='_community_communitypost_comments_+', to='community.CommunityPost')), + ('community', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='community_posts', to='community.community')), + ('followed_by', models.ManyToManyField(blank=True, related_name='communitypost_followers', to='users.UserProfile')), + ('interests', models.ManyToManyField(blank=True, related_name='communitypost_interest', to='achievements.Interest')), + ('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='communitypost_parent', to='community.communitypost')), + ('posted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='communitypost_postedby', to='users.userprofile')), + ], + options={ + 'verbose_name': 'Community Post', + 'verbose_name_plural': 'Community Posts', + 'ordering': ('-time_of_creation',), + }, + ), + migrations.CreateModel( + name='CommunityPostUserReaction', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('time_of_creation', models.DateTimeField(auto_now_add=True)), + ('reaction', models.IntegerField(default=0)), + ('communitypost', models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='communitypost_communitypostreaction', to='community.communitypost')), + ('user', models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='user_communitypostreaction', to='users.userprofile')), + ], + options={ + 'verbose_name': 'Community Post User Reaction', + 'verbose_name_plural': 'Community Post User Reactions', + }, + ), + migrations.AddField( + model_name='communitypost', + name='reacted_by', + field=models.ManyToManyField(blank=True, related_name='communitypost_reaction', through='community.CommunityPostUserReaction', to='users.UserProfile'), + ), + migrations.AddField( + model_name='communitypost', + name='tag_body', + field=models.ManyToManyField(blank=True, related_name='Tagged_Body', to='bodies.Body'), + ), + migrations.AddField( + model_name='communitypost', + name='tag_location', + field=models.ManyToManyField(blank=True, related_name='communitypost_tagloc', to='locations.Location'), + ), + migrations.AddField( + model_name='communitypost', + name='tag_user', + field=models.ManyToManyField(blank=True, related_name='communitypost_taguser', to='users.UserProfile'), + ), + ] diff --git a/community/migrations/0002_alter_communitypost_parent.py b/community/migrations/0002_alter_communitypost_parent.py new file mode 100644 index 00000000..05b0de0f --- /dev/null +++ b/community/migrations/0002_alter_communitypost_parent.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-06-30 11:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='communitypost_parent', to='community.communitypost'), + ), + ] diff --git a/community/migrations/0003_auto_20220703_0229.py b/community/migrations/0003_auto_20220703_0229.py new file mode 100644 index 00000000..7c584ee1 --- /dev/null +++ b/community/migrations/0003_auto_20220703_0229.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.13 on 2022-07-02 20:59 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('bodies', '0023_body_canonical_name'), + ('users', '0038_auto_20210606_2237'), + ('community', '0002_alter_communitypost_parent'), + ] + + operations = [ + migrations.AddField( + model_name='community', + name='body', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='community_body', to='bodies.body'), + ), + migrations.AlterField( + model_name='communitypostuserreaction', + name='communitypost', + field=models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='ucpr', to='community.communitypost'), + ), + migrations.AlterField( + model_name='communitypostuserreaction', + name='user', + field=models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='ucpr', to='users.userprofile'), + ), + ] diff --git a/community/migrations/0004_remove_community_followers_count.py b/community/migrations/0004_remove_community_followers_count.py new file mode 100644 index 00000000..ae9a901b --- /dev/null +++ b/community/migrations/0004_remove_community_followers_count.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-07-02 21:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0003_auto_20220703_0229'), + ] + + operations = [ + migrations.RemoveField( + model_name='community', + name='followers_count', + ), + ] diff --git a/community/migrations/0005_alter_communitypost_community.py b/community/migrations/0005_alter_communitypost_community.py new file mode 100644 index 00000000..690b135c --- /dev/null +++ b/community/migrations/0005_alter_communitypost_community.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-02 21:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0004_remove_community_followers_count'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='community', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='community.community'), + ), + ] diff --git a/community/migrations/0006_rename_threadrank_communitypost_thread_rank.py b/community/migrations/0006_rename_threadrank_communitypost_thread_rank.py new file mode 100644 index 00000000..a3c76c48 --- /dev/null +++ b/community/migrations/0006_rename_threadrank_communitypost_thread_rank.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-02 22:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0005_alter_communitypost_community'), + ] + + operations = [ + migrations.RenameField( + model_name='communitypost', + old_name='threadRank', + new_name='thread_rank', + ), + ] diff --git a/community/migrations/0007_auto_20220703_0347.py b/community/migrations/0007_auto_20220703_0347.py new file mode 100644 index 00000000..fa79cd8c --- /dev/null +++ b/community/migrations/0007_auto_20220703_0347.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2022-07-02 22:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0006_rename_threadrank_communitypost_thread_rank'), + ] + + operations = [ + migrations.RemoveField( + model_name='communitypost', + name='comments', + ), + migrations.AlterField( + model_name='communitypost', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='community.communitypost'), + ), + ] diff --git a/community/migrations/0008_alter_communitypost_community.py b/community/migrations/0008_alter_communitypost_community.py new file mode 100644 index 00000000..caa8c59c --- /dev/null +++ b/community/migrations/0008_alter_communitypost_community.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-06 10:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0007_auto_20220703_0347'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='community', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='community.community'), + ), + ] diff --git a/community/migrations/0009_community_followers.py b/community/migrations/0009_community_followers.py new file mode 100644 index 00000000..e2061f89 --- /dev/null +++ b/community/migrations/0009_community_followers.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-07 18:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0008_alter_communitypost_community'), + ] + + operations = [ + migrations.AddField( + model_name='community', + name='followers', + field=models.IntegerField(default=0), + ), + ] diff --git a/community/migrations/0010_community_featured.py b/community/migrations/0010_community_featured.py new file mode 100644 index 00000000..9a52025c --- /dev/null +++ b/community/migrations/0010_community_featured.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-14 08:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0009_community_followers'), + ] + + operations = [ + migrations.AddField( + model_name='community', + name='featured', + field=models.BooleanField(default=False), + ), + ] diff --git a/community/migrations/0011_auto_20220714_1428.py b/community/migrations/0011_auto_20220714_1428.py new file mode 100644 index 00000000..46257079 --- /dev/null +++ b/community/migrations/0011_auto_20220714_1428.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.13 on 2022-07-14 08:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0010_community_featured'), + ] + + operations = [ + migrations.RemoveField( + model_name='community', + name='featured', + ), + migrations.AddField( + model_name='communitypost', + name='featured', + field=models.BooleanField(default=False), + ), + ] diff --git a/community/migrations/0012_auto_20220714_2130.py b/community/migrations/0012_auto_20220714_2130.py new file mode 100644 index 00000000..52b77161 --- /dev/null +++ b/community/migrations/0012_auto_20220714_2130.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.10 on 2022-07-14 16:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0011_auto_20220714_1428'), + ] + + operations = [ + migrations.AddField( + model_name='communitypost', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='communitypost', + name='reported', + field=models.BooleanField(default=False), + ), + ] diff --git a/community/migrations/0013_communitypost_anonymous.py b/community/migrations/0013_communitypost_anonymous.py new file mode 100644 index 00000000..9db4fcfb --- /dev/null +++ b/community/migrations/0013_communitypost_anonymous.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-15 13:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0012_auto_20220714_2130'), + ] + + operations = [ + migrations.AddField( + model_name='communitypost', + name='anonymous', + field=models.BooleanField(default=False), + ), + ] diff --git a/community/migrations/0014_alter_communitypost_thread_rank.py b/community/migrations/0014_alter_communitypost_thread_rank.py new file mode 100644 index 00000000..122ba497 --- /dev/null +++ b/community/migrations/0014_alter_communitypost_thread_rank.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-17 14:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0013_communitypost_anonymous'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='thread_rank', + field=models.IntegerField(blank=True, default=1), + ), + ] diff --git a/community/migrations/0015_alter_communitypost_thread_rank.py b/community/migrations/0015_alter_communitypost_thread_rank.py new file mode 100644 index 00000000..d49981f6 --- /dev/null +++ b/community/migrations/0015_alter_communitypost_thread_rank.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-17 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0014_alter_communitypost_thread_rank'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='thread_rank', + field=models.IntegerField(default=1, null=True), + ), + ] diff --git a/community/migrations/0016_alter_communitypost_status.py b/community/migrations/0016_alter_communitypost_status.py new file mode 100644 index 00000000..ef0774b9 --- /dev/null +++ b/community/migrations/0016_alter_communitypost_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-25 12:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0015_alter_communitypost_thread_rank'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='status', + field=models.IntegerField(blank=True, default=0, null=True), + ), + ] diff --git a/community/migrations/0017_rename_hidden_communitypost_deleted.py b/community/migrations/0017_rename_hidden_communitypost_deleted.py new file mode 100644 index 00000000..f9c1b73b --- /dev/null +++ b/community/migrations/0017_rename_hidden_communitypost_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-07-29 12:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0016_alter_communitypost_status'), + ] + + operations = [ + migrations.RenameField( + model_name='communitypost', + old_name='hidden', + new_name='deleted', + ), + ] diff --git a/community/migrations/0018_communitypost_reported_by.py b/community/migrations/0018_communitypost_reported_by.py new file mode 100644 index 00000000..4c82c7f3 --- /dev/null +++ b/community/migrations/0018_communitypost_reported_by.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-08-22 18:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0040_remove_userprofile_followed_communities'), + ('community', '0017_rename_hidden_communitypost_deleted'), + ] + + operations = [ + migrations.AddField( + model_name='communitypost', + name='reported_by', + field=models.ManyToManyField(blank=True, related_name='communitypost_report', through='community.CommunityPost', to='users.UserProfile'), + ), + ] diff --git a/community/migrations/0019_remove_communitypost_reported.py b/community/migrations/0019_remove_communitypost_reported.py new file mode 100644 index 00000000..ebd1c98f --- /dev/null +++ b/community/migrations/0019_remove_communitypost_reported.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-08-22 18:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0018_communitypost_reported_by'), + ] + + operations = [ + migrations.RemoveField( + model_name='communitypost', + name='reported', + ), + ] diff --git a/community/migrations/0020_remove_communitypost_reported_by.py b/community/migrations/0020_remove_communitypost_reported_by.py new file mode 100644 index 00000000..0aa6c5f6 --- /dev/null +++ b/community/migrations/0020_remove_communitypost_reported_by.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-08-22 18:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0019_remove_communitypost_reported'), + ] + + operations = [ + migrations.RemoveField( + model_name='communitypost', + name='reported_by', + ), + ] diff --git a/community/migrations/0021_communitypost_reported_by.py b/community/migrations/0021_communitypost_reported_by.py new file mode 100644 index 00000000..0b7ed486 --- /dev/null +++ b/community/migrations/0021_communitypost_reported_by.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-08-22 18:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0040_remove_userprofile_followed_communities'), + ('community', '0020_remove_communitypost_reported_by'), + ] + + operations = [ + migrations.AddField( + model_name='communitypost', + name='reported_by', + field=models.ManyToManyField(blank=True, related_name='posts_reported', to='users.UserProfile'), + ), + ] diff --git a/community/migrations/0022_alter_communitypost_anonymous.py b/community/migrations/0022_alter_communitypost_anonymous.py new file mode 100644 index 00000000..409b6fec --- /dev/null +++ b/community/migrations/0022_alter_communitypost_anonymous.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-08-24 12:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0021_communitypost_reported_by'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='anonymous', + field=models.BooleanField(blank=True, default=False), + ), + ] diff --git a/community/migrations/0022_communitypost_reports.py b/community/migrations/0022_communitypost_reports.py new file mode 100644 index 00000000..8cad28a7 --- /dev/null +++ b/community/migrations/0022_communitypost_reports.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-27 15:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0021_communitypost_reported_by'), + ] + + operations = [ + migrations.AddField( + model_name='communitypost', + name='reports', + field=models.IntegerField(default=0, null=True), + ), + ] diff --git a/community/migrations/0023_remove_communitypost_reports.py b/community/migrations/0023_remove_communitypost_reports.py new file mode 100644 index 00000000..e4ca5aa8 --- /dev/null +++ b/community/migrations/0023_remove_communitypost_reports.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-08-27 15:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0022_communitypost_reports'), + ] + + operations = [ + migrations.RemoveField( + model_name='communitypost', + name='reports', + ), + ] diff --git a/community/migrations/0024_communitypost_ignored.py b/community/migrations/0024_communitypost_ignored.py new file mode 100644 index 00000000..734aeb1e --- /dev/null +++ b/community/migrations/0024_communitypost_ignored.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-29 18:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0023_remove_communitypost_reports'), + ] + + operations = [ + migrations.AddField( + model_name='communitypost', + name='ignored', + field=models.BooleanField(default=False), + ), + ] diff --git a/community/migrations/0025_alter_communitypost_anonymous.py b/community/migrations/0025_alter_communitypost_anonymous.py new file mode 100644 index 00000000..2146c38f --- /dev/null +++ b/community/migrations/0025_alter_communitypost_anonymous.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-08-30 09:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0024_communitypost_ignored'), + ] + + operations = [ + migrations.AlterField( + model_name='communitypost', + name='anonymous', + field=models.BooleanField(blank=True, default=False), + ), + ] diff --git a/community/migrations/0026_merge_20220830_1636.py b/community/migrations/0026_merge_20220830_1636.py new file mode 100644 index 00000000..41f13259 --- /dev/null +++ b/community/migrations/0026_merge_20220830_1636.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.14 on 2022-08-30 11:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0022_alter_communitypost_anonymous'), + ('community', '0025_alter_communitypost_anonymous'), + ] + + operations = [ + ] diff --git a/community/migrations/0027_auto_20221003_1705.py b/community/migrations/0027_auto_20221003_1705.py new file mode 100644 index 00000000..85437fec --- /dev/null +++ b/community/migrations/0027_auto_20221003_1705.py @@ -0,0 +1,61 @@ +# Generated by Django 3.2.13 on 2022-10-03 11:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0040_remove_userprofile_followed_communities'), + ('bodies', '0023_body_canonical_name'), + ('community', '0026_merge_20220830_1636'), + ] + + operations = [ + migrations.AlterField( + model_name='community', + name='about', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='community', + name='body', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='community_body', to='bodies.body'), + ), + migrations.AlterField( + model_name='community', + name='description', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='communitypost', + name='community', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='community.community'), + ), + migrations.AlterField( + model_name='communitypost', + name='content', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='communitypost', + name='posted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='communitypost_postedby', to='users.userprofile'), + ), + migrations.AlterField( + model_name='communitypost', + name='thread_rank', + field=models.IntegerField(blank=True, default=1, null=True), + ), + migrations.AlterField( + model_name='communitypostuserreaction', + name='communitypost', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ucpr', to='community.communitypost'), + ), + migrations.AlterField( + model_name='communitypostuserreaction', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ucpr', to='users.userprofile'), + ), + ] diff --git a/community/migrations/0028_auto_20221003_2130.py b/community/migrations/0028_auto_20221003_2130.py new file mode 100644 index 00000000..40a56f8a --- /dev/null +++ b/community/migrations/0028_auto_20221003_2130.py @@ -0,0 +1,61 @@ +# Generated by Django 3.2.13 on 2022-10-03 16:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('locations', '0014_location_str_id'), + ('bodies', '0023_body_canonical_name'), + ('users', '0040_remove_userprofile_followed_communities'), + ('community', '0027_auto_20221003_1705'), + ] + + operations = [ + migrations.RemoveField( + model_name='communitypost', + name='view_count', + ), + migrations.AlterField( + model_name='communitypost', + name='deleted', + field=models.BooleanField(blank=True, default=False, null=True), + ), + migrations.AlterField( + model_name='communitypost', + name='featured', + field=models.BooleanField(blank=True, default=False, null=True), + ), + migrations.AlterField( + model_name='communitypost', + name='followed_by', + field=models.ManyToManyField(blank=True, related_name='followedposts', to='users.UserProfile'), + ), + migrations.AlterField( + model_name='communitypost', + name='ignored', + field=models.BooleanField(blank=True, default=False, null=True), + ), + migrations.AlterField( + model_name='communitypost', + name='posted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='communityposts', to='users.userprofile'), + ), + migrations.AlterField( + model_name='communitypost', + name='tag_body', + field=models.ManyToManyField(blank=True, related_name='taggedcommunityposts', to='bodies.Body'), + ), + migrations.AlterField( + model_name='communitypost', + name='tag_location', + field=models.ManyToManyField(blank=True, related_name='taggedcommunityposts', to='locations.Location'), + ), + migrations.AlterField( + model_name='communitypost', + name='tag_user', + field=models.ManyToManyField(blank=True, related_name='taggedcommunitypost', to='users.UserProfile'), + ), + ] diff --git a/community/migrations/__init__.py b/community/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/models.py b/community/models.py new file mode 100644 index 00000000..51ac991c --- /dev/null +++ b/community/models.py @@ -0,0 +1,103 @@ +from datetime import datetime +from uuid import uuid4 +from django.db import models +from helpers.misc import get_url_friendly + +class Community(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + str_id = models.CharField(max_length=58, editable=False, null=True) + name = models.CharField(max_length=100) + about = models.TextField(null=True, blank=True) + description = models.TextField(null=True, blank=True) + logo_image = models.URLField(blank=True, null=True) + cover_image = models.URLField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + body = models.ForeignKey('bodies.Body', on_delete=models.CASCADE, related_name='community_body') + followers = models.IntegerField(default=0) + + def __str__(self): + return str(self.name) + + def save(self, *args, **kwargs): # pylint: disable=W0222 + self.str_id = get_url_friendly(self.name) + "-" + str(self.id)[:8] + super().save(*args, **kwargs) + + class Meta: + verbose_name = "Community" + verbose_name_plural = "Communities" + ordering = ("-created_at",) + +class CommunityPost(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + str_id = models.CharField(max_length=58, editable=False, null=True) + time_of_creation = models.DateTimeField(auto_now_add=True) + time_of_modification = models.DateTimeField(auto_now=True) + content = models.TextField(null=True, blank=True) + image_url = models.TextField(blank=True, null=True) + reported_by = models.ManyToManyField('users.UserProfile', related_name='posts_reported', blank=True) + reacted_by = models.ManyToManyField( + 'users.UserProfile', through='CommunityPostUserReaction', + related_name='communitypost_reaction', blank=True) + featured = models.BooleanField(default=False, null=True, blank=True) + deleted = models.BooleanField(default=False, null=True, blank=True) + community = models.ForeignKey(Community, on_delete=models.CASCADE, related_name='posts') + ignored = models.BooleanField(default=False, null=True, blank=True) + thread_rank = models.IntegerField(default=1, null=True, blank=True) + parent = models.ForeignKey("self", blank=True, null=True, + related_name="comments", on_delete=models.CASCADE) + # comments = models.ManyToManyField( + # "self", blank=True, related_name="community_post_comments") + + tag_user = models.ManyToManyField("users.UserProfile", related_name="taggedcommunitypost", blank=True) + tag_body = models.ManyToManyField('bodies.Body', related_name="taggedcommunityposts", blank=True) + tag_location = models.ManyToManyField('locations.Location', related_name='taggedcommunityposts', blank=True) + followed_by = models.ManyToManyField("users.UserProfile", related_name="followedposts", blank=True) + anonymous = models.BooleanField(default=False, blank=True) + posted_by = models.ForeignKey('users.UserProfile', on_delete=models.CASCADE, + related_name='communityposts') + interests = models.ManyToManyField('achievements.Interest', related_name='communitypost_interest', blank=True) + + """ Status + 0 - Pending + 1 - Approved + 2 - Rejected + 3 - Reported + + """ + status = models.IntegerField(null=True, blank=True, default=0) + + def save(self, *args, **kwargs): # pylint: disable=W0222 + self.str_id = get_url_friendly(str(datetime.now())) + "-" + str(self.id)[:8] + if not self.ignored: + if self.reported_by.count() > 1: + self.status = 3 + super().save(*args, **kwargs) + + def __str__(self) -> str: + return self.content[:100] + + class Meta: + verbose_name = "Community Post" + verbose_name_plural = "Community Posts" + ordering = ("-time_of_creation",) + +class CommunityPostUserReaction(models.Model): + """ Reaction: + 0 - Like + 1 - Love + 2 - Haha + 3 - Wow + 4 - Sad + 5 - Angry + """ + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + time_of_creation = models.DateTimeField(auto_now_add=True) + + user = models.ForeignKey('users.UserProfile', on_delete=models.CASCADE, related_name='ucpr') + communitypost = models.ForeignKey(CommunityPost, on_delete=models.CASCADE, related_name='ucpr') + reaction = models.IntegerField(default=0) + + class Meta: + verbose_name = "Community Post User Reaction" + verbose_name_plural = "Community Post User Reactions" diff --git a/community/serializer_min.py b/community/serializer_min.py new file mode 100644 index 00000000..024ae70e --- /dev/null +++ b/community/serializer_min.py @@ -0,0 +1,125 @@ +"""Minimal serializer for Body.""" +from rest_framework import serializers +from achievements.serializers import InterestSerializer +from bodies.serializer_min import BodySerializerMin +from community.models import Community, CommunityPost +from users.serializers import UserProfileSerializer + +class CommunitySerializerMin(serializers.ModelSerializer): + """Minimal serializer for Community.""" + + followers_count = serializers.SerializerMethodField() + is_user_following = serializers.SerializerMethodField() + + def get_followers_count(self, obj): + """Get followers of community.""" + if obj.body is None: + return 0 + return obj.body.followers.count() + + def get_is_user_following(self, obj): + """Get the current user's reaction on the community post """ + request = self.context['request'] if 'request' in self.context else None + if request and request.user.is_authenticated: + profile = request.user.profile + return profile.followed_bodies.filter(id=obj.body.id).exists() + return False + + class Meta: + model = Community + fields = ('id', 'str_id', 'name', 'about', 'cover_image', + 'logo_image', 'followers_count', 'body', 'is_user_following') + +class CommunityPostSerializerMin(serializers.ModelSerializer): + """Minimal serializer for Body.""" + + reactions_count = serializers.SerializerMethodField() + user_reaction = serializers.SerializerMethodField() + comments_count = serializers.SerializerMethodField() + posted_by = UserProfileSerializer(read_only=True) + image_url = serializers.SerializerMethodField() + most_liked_comment = serializers.SerializerMethodField() + community = CommunitySerializerMin(read_only=True) + tag_body = BodySerializerMin(read_only=True, many=True) + tag_user = UserProfileSerializer(read_only=True, many=True) + interests = InterestSerializer(read_only=True, many=True) + has_user_reported = serializers.SerializerMethodField() + + def get_most_liked_comment(self, obj): + """Get the most liked comment of the community post """ + queryset = obj.comments.filter(deleted=False, status=1) + if len(queryset) == 0: + return None + max = 0 + most_liked_comment = None + for comment in queryset: + if comment.ucpr.count() >= max: + max = comment.ucpr.count() + most_liked_comment = comment + + return CommunityPostSerializerMin(most_liked_comment).data + + def get_image_url(self, obj): + """Get the image url of the community post """ + return obj.image_url.split(',') if obj.image_url else None + + @staticmethod + def setup_eager_loading(queryset, request): + """Perform necessary eager loading of data.""" + + # Prefetch body child relations + + # Annotate followers count + + return queryset + + @staticmethod + def get_reactions_count(obj): + """Get number of user reactions on community post item.""" + # Get all UCPR for news item + ucprs = obj.ucpr.all() + + # Count for each type + reaction_counts = {t: 0 for t in range(0, 6)} + for ucpr in ucprs: + if ucpr.reaction >= 0 and ucpr.reaction < 6: + reaction_counts[ucpr.reaction] += 1 + + return reaction_counts + + @staticmethod + def get_comments_count(obj): + """Get number of comments on community post item.""" + if not obj.comments.exists() or len(obj.comments.filter(deleted=False, status=1)) == 0: + return 0 + count = 0 + + for comment in obj.comments.filter(deleted=False, status=1): + count += 1 + count += CommunityPostSerializerMin.get_comments_count(comment) + + return count + + def get_user_reaction(self, obj): + """Get the current user's reaction on the community post """ + request = self.context['request'] if 'request' in self.context else None + if request and request.user.is_authenticated: + profile = request.user.profile + return next((u.reaction for u in obj.ucpr.all() if u.user_id == profile.id), -1) + return -1 + + def get_has_user_reported(self, obj): + """Get the current user's report on the community post """ + request = self.context['request'] if 'request' in self.context else None + if request and request.user.is_authenticated: + profile = request.user.profile + + return obj.reported_by.filter(id=profile.id).exists() + return False + + class Meta: + model = CommunityPost + fields = ('id', 'str_id', 'content', 'posted_by', + 'reactions_count', 'user_reaction', 'comments_count', 'time_of_creation', 'time_of_modification', + 'image_url', 'most_liked_comment', 'thread_rank', 'community', 'status', 'tag_body', 'tag_user', + 'interests', 'featured', 'deleted', 'anonymous', 'reported_by', 'has_user_reported') diff --git a/community/serializers.py b/community/serializers.py new file mode 100644 index 00000000..87b30bba --- /dev/null +++ b/community/serializers.py @@ -0,0 +1,113 @@ +from rest_framework import serializers +from achievements.models import Interest +from community.models import Community, CommunityPost +from community.serializer_min import CommunityPostSerializerMin +from roles.serializers import RoleSerializerMin +from users.models import UserProfile +from bodies.models import Body + +class CommunitySerializers(serializers.ModelSerializer): + + followers_count = serializers.SerializerMethodField() + is_user_following = serializers.SerializerMethodField() + roles = RoleSerializerMin(many=True, read_only=True, source='body.roles') + posts = serializers.SerializerMethodField() + featured_posts = serializers.SerializerMethodField() + + def get_featured_posts(self, obj): + """Get the featured posts of community""" + queryset = obj.posts.filter(featured=True, deleted=False, status=1) + return CommunityPostSerializerMin(queryset, many=True).data + + def get_posts(self, obj): + """Get the posts of the community """ + queryset = obj.posts.filter(thread_rank=1) + return CommunityPostSerializerMin(queryset, many=True).data + + def get_followers_count(self, obj): + """Get followers of body.""" + if obj.body is None: + return 0 + return obj.body.followers.count() + + def get_is_user_following(self, obj): + """Get the current user's reaction on the community post """ + request = self.context['request'] if 'request' in self.context else None + if request and request.user.is_authenticated: + profile = request.user.profile + return profile.followed_bodies.filter(id=obj.body.id).exists() + return False + + class Meta: + model = Community + fields = ('id', 'str_id', 'name', 'about', 'description', 'created_at', 'updated_at', + 'cover_image', 'logo_image', 'followers_count', 'is_user_following', 'roles', + 'posts', 'body', 'featured_posts') + + @staticmethod + def setup_eager_loading(queryset, request): + """Perform necessary eager loading of data.""" + + # Prefetch body child relations + + # Annotate followers count + + return queryset + + +class CommunityPostSerializers(CommunityPostSerializerMin): + comments = serializers.SerializerMethodField() + + def get_comments(self, obj): + comments = obj.comments.filter(deleted=False, status=1) + return CommunityPostSerializerMin(comments, many=True).data + + class Meta: + model = CommunityPost + fields = ('id', 'str_id', 'content', 'posted_by', + 'reactions_count', 'user_reaction', 'comments_count', 'time_of_creation', 'time_of_modification', + 'image_url', 'comments', 'thread_rank', 'community', 'status', 'tag_body', 'tag_user', 'interests', + 'featured', 'deleted', 'anonymous', 'reported_by') + + def create(self, validated_data): + data = self.context["request"].data + if 'parent' in data and data['parent']: + parent = CommunityPost.objects.get(id=data['parent']) + validated_data['parent'] = parent + validated_data["thread_rank"] = parent.thread_rank + 1 + validated_data["status"] = 1 + else: + validated_data['parent'] = None + validated_data["thread_rank"] = 1 + validated_data["status"] = 0 + validated_data['image_url'] = ",".join(data["image_url"]) if 'image_url' in data else "" + if 'tag_user' in data and data["tag_user"]: + validated_data["tag_user"] = [UserProfile.objects.get(id=i['id']) for i in data['tag_user']] + if 'tag_body' in data and data['tag_body']: + validated_data['tag_body'] = [Body.objects.get(id=i['id']) for i in data['tag_body']] + if 'interests' in data and data['interests']: + validated_data['interests'] = [Interest.objects.get(id=i['id']) for i in data['interests']] + validated_data['posted_by'] = self.context['request'].user.profile + validated_data['community'] = Community.objects.get(id=data['community']['id']) + return super().create(validated_data) + + def update(self, instance, validated_data): + data = self.context["request"].data + if 'tag_user' in data and data["tag_user"]: + validated_data["tag_user"] = [UserProfile.objects.get(id=i['id']) for i in data['tag_user']] + if 'tag_body' in data and data['tag_body']: + validated_data['tag_body'] = [Body.objects.get(id=i['id']) for i in data['tag_body']] + if 'interests' in data and data['interests']: + validated_data['interests'] = [Interest.objects.get(id=i['id']) for i in data['interests']] + validated_data["status"] = 0 + validated_data["deleted"] = False + validated_data["featured"] = False + validated_data['image_url'] = ",".join(data["image_url"]) if 'image_url' in data and data['image_url'] else "" + return super().update(instance, validated_data) + + def destroy(self, instance, validated_data): + data = self.context["request"].data + validated_data["status"] = 0 + validated_data["deleted"] = True + validated_data['image_url'] = ",".join(data["image_url"]) if 'image_url' in data else "" + return super().update(instance, validated_data) diff --git a/community/tests.py b/community/tests.py new file mode 100644 index 00000000..0f814443 --- /dev/null +++ b/community/tests.py @@ -0,0 +1,220 @@ +"""Unit tests for Events.""" +from django.test import TransactionTestCase +from rest_framework.test import APIClient +from community.models import Community, CommunityPost +from roles.models import BodyRole +from login.tests import get_new_user +from helpers.test_helpers import create_body +from helpers.test_helpers import create_community +from helpers.test_helpers import create_communitypost +# pylint: disable=R0902 + +class CommunityTestCase(TransactionTestCase): + """Check if we can create communities and link communityposts.""" + + def setUp(self): + # Fake authenticate + self.user1 = get_new_user() + self.client1 = APIClient() + self.client1.force_authenticate(self.user1) + + self.user2 = get_new_user() + self.client2 = APIClient() + self.client2.force_authenticate(self.user2) + + self.test_body_1 = create_body() + self.test_body_2 = create_body() + + self.body_1_role = BodyRole.objects.create( + name="Body1Role", body=self.test_body_1, permissions='AppP,ModC') + self.user1.profile.roles.add(self.body_1_role) + + self.test_community_1 = create_community(body=self.test_body_1) + self.test_community_2 = create_community(body=self.test_body_2) + + self.test_communitypost_11 = create_communitypost( + community=self.test_community_1, posted_by=self.user1.profile, status=1) + self.test_communitypost_12 = create_communitypost( + community=self.test_community_1, posted_by=self.user2.profile, status=1) + + self.test_communitypost_21 = create_communitypost( + community=self.test_community_2, posted_by=self.user1.profile, status=1) + + def test_community_list(self): + """Test if communities can be listed.""" + url = '/api/events' + response = self.client1.get(url) + self.assertEqual(response.status_code, 200) + self.assertGreater(len(response.data), 0) + + def test_community_get(self): + """Test getting the community with id or str_id.""" + community = Community.objects.create(name="Test #Community 123!", body=create_body()) + + url = '/api/communities/' + str(community.id) + response = self.client1.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['name'], community.name) + + url = '/api/communities/test-community-123-' + str(community.id)[:8] + response = self.client1.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['name'], community.name) + + def test_communitypost_alllist(self): + """Test if communityposts can be listed.""" + url = '/api/communityposts?status=1' + response = self.client1.get(url, format='json') + data = response.data['data'] + self.assertEqual(response.status_code, 200) + self.assertGreater(response.data['count'], 0) + self.assertListEqual(list(map(lambda x: (x['status'], x['deleted'], x['thread_rank']), data)), + [(1, False, 1)] * response.data['count']) + + def test_communitypost_yourlist(self): + url = '/api/communityposts' + response = self.client1.get(url, format='json') + data = response.data['data'] + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], CommunityPost.objects.filter( + thread_rank=1, posted_by=self.user1.profile).count()) + self.assertListEqual(list(map(lambda x: x['posted_by']['name'], data)), + [self.user1.profile.name] * response.data['count']) + + def test_communitypost_pendinglist(self): + url = '/api/communityposts?status=0' + response = self.client1.get(url, format='json') + data = response.data['data'] + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], CommunityPost.objects.filter( + thread_rank=1, status=0).count()) + self.assertListEqual(list(map(lambda x: (x['status'], x['thread_rank'], x['deleted']), data)), + [(0, 1, False)] * response.data['count']) + + def test_communitypost_reportedlist(self): + pass + + def test_communitypost_create(self): + """Test if communityposts can be created.""" + url = '/api/communityposts' + data = { + "content": "Test content 1", + "community": { + "id": self.test_community_1.id, + }, + } + response = self.client1.post(url, data, format='json') + self.assertEqual(response.status_code, 201) + self.assertEqual(response.data['posted_by']['name'], self.user1.profile.name) + + url = '/api/communityposts' + data = { + "content": "Test content 2", + "community": { + "id": self.test_community_1.id, + }, + } + response = self.client2.post(url, data, format='json') + self.assertEqual(response.status_code, 201) + self.assertEqual(response.data['posted_by']['name'], self.user2.profile.name) + + def test_communitypost_get(self): + """Test getting the community with id or str_id.""" + communitypost = CommunityPost.objects.create( + content="Test #CommunityPost 123!", community=self.test_community_1, posted_by=self.user1.profile) + + url = '/api/communityposts/' + str(communitypost.id) + response = self.client1.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['content'], communitypost.content) + + url = '/api/communityposts/' + str(communitypost.str_id) + response = self.client1.get(url, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['content'], communitypost.content) + + def test_communitypost_edit(self): + url = '/api/communityposts/' + str(self.test_communitypost_11.id) + data = { + "content": "Test content 1 edited", + } + response = self.client2.put(url, data, format='json') + self.assertEqual(response.status_code, 403) + + url = '/api/communityposts/' + str(self.test_communitypost_11.id) + data = { + "content": "Test content 1 edited", + } + response = self.client1.put(url, data, format='json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['content'], data['content']) + + def test_communitypost_moderation(self): + post1 = create_communitypost(community=self.test_community_1, posted_by=self.user1.profile) + + # Reject + url = '/api/communityposts/moderator/' + str(post1.id) + data = { + "status": 2, + } + response = self.client2.put(url, data, format='json') + self.assertEqual(response.status_code, 403) + + url = '/api/communityposts/moderator/' + str(post1.id) + data = { + "status": 2, + } + response = self.client1.put(url, data, format='json') + self.assertEqual(response.status_code, 200) + + # Accept + url = '/api/communityposts/moderator/' + str(post1.id) + data = { + "status": 1, + } + response = self.client2.put(url, data, format='json') + self.assertEqual(response.status_code, 403) + + url = '/api/communityposts/moderator/' + str(post1.id) + data = { + "status": 1, + } + response = self.client1.put(url, data, format='json') + self.assertEqual(response.status_code, 200) + + def test_communitypost_feature(self): + url = '/api/communityposts/feature/' + str(self.test_communitypost_11.id) + data = { + "is_featured": True, + } + response = self.client2.put(url, data, format='json') + self.assertEqual(response.status_code, 403) + + url = '/api/communityposts/feature/' + str(self.test_communitypost_11.id) + data = { + "is_featured": True, + } + response = self.client1.put(url, data, format='json') + self.assertEqual(response.status_code, 200) + + def test_communitypost_delete(self): + url = '/api/communityposts/delete/' + str(self.test_communitypost_11.id) + response = self.client2.put(url, format='json') + self.assertEqual(response.status_code, 403) + + url = '/api/communityposts/delete/' + str(self.test_communitypost_12.id) + response = self.client2.put(url, format='json') + self.assertEqual(response.status_code, 200) + + url = '/api/communityposts/delete/' + str(self.test_communitypost_12.id) + response = self.client1.put(url, format='json') + self.assertEqual(response.status_code, 200) + + def test_communitypost_report(self): + url = '/api/communityposts/report/' + str(self.test_communitypost_12.id) + response = self.client1.put(url, format='json') + self.assertEqual(response.status_code, 200) + + url = '/api/communityposts/report/' + str(self.test_communitypost_12.id) + response = self.client1.put(url, format='json') + self.assertEqual(response.status_code, 200) diff --git a/community/urls.py b/community/urls.py new file mode 100644 index 00000000..c3407a9a --- /dev/null +++ b/community/urls.py @@ -0,0 +1,29 @@ +"""URLs for communities.""" +from django.urls import path +from community.views import CommunityViewSet, PostViewSet, ModeratorViewSet + +urlpatterns = [ + path('communities', CommunityViewSet.as_view({ + 'get': 'list', + })), # viewing the list of communities + + path('communities/', CommunityViewSet.as_view({ + 'get': 'retrieve', + })), # viewing a particular community + + path('communityposts', PostViewSet.as_view({ + 'get': 'list', 'post': 'create' + })), # viewing, creating, updating and deleting the list of posts in their minimum view + + path('communityposts/', PostViewSet.as_view({ + 'get': 'retrieve_full', 'put': 'update' + })), # to get the full view of a post + + path('communityposts/moderator/', ModeratorViewSet.as_view({ + 'put': 'change_status' + })), # manages all the privileges of a moderator.. changes status + + path('communityposts//', PostViewSet.as_view({ + 'put': 'perform_action' + })), # setting featured posts +] diff --git a/community/views.py b/community/views.py new file mode 100644 index 00000000..c8021d9f --- /dev/null +++ b/community/views.py @@ -0,0 +1,205 @@ +from uuid import UUID +from rest_framework.response import Response +from rest_framework import viewsets +from django.shortcuts import get_object_or_404 +from community.models import Community +from community.models import CommunityPost +from community.serializer_min import CommunitySerializerMin, CommunityPostSerializerMin +from community.serializers import CommunitySerializers, CommunityPostSerializers +from roles.helpers import user_has_privilege +from roles.helpers import login_required_ajax +from roles.helpers import forbidden_no_privileges +from helpers.misc import query_from_num +from helpers.misc import query_search + +class ModeratorViewSet(viewsets.ModelViewSet): + queryset = CommunityPost.objects + serializer_class = CommunityPostSerializers + serializer_class_min = CommunityPostSerializerMin + + def get_community_post(self, pk): + """Get a community post from pk uuid or strid.""" + try: + UUID(pk, version=4) + return get_object_or_404(self.queryset, id=pk) + except ValueError: + return get_object_or_404(self.queryset, str_id=pk) + + def change_status(self, request, pk): + post = self.get_community_post(pk) + + if ((user_has_privilege(request.user.profile, post.community.body.id, 'AppP') and post.thread_rank == 1) + or (user_has_privilege(request.user.profile, post.community.body.id, 'ModC') and post.thread_rank > 1)): + # Get query param + status = request.data["status"] + if status is None: + return Response({"message": "{?action} is required"}, status=400) + if post.status == 3: + post.ignored = True + + # Check possible actions + post.status = status + post.save() + return Response({"message": "Status changed"}) + + return forbidden_no_privileges() + + +class PostViewSet(viewsets.ModelViewSet): + """Post""" + + queryset = CommunityPost.objects + serializer_class = CommunityPostSerializers + serializer_class_min = CommunityPostSerializerMin + + def get_serializer_context(self): + return {'request': self.request} + + @login_required_ajax + def retrieve_full(self, request, pk): + """Get full Post. + Get by `uuid` or `str_id`""" + + self.queryset = CommunityPostSerializers.setup_eager_loading(self.queryset, request) + post = self.get_community_post(pk) + serialized = CommunityPostSerializers(post, context={'request': request}).data + + return Response(serialized) + + @login_required_ajax + def list(self, request): + """List Of Posts. + List fresh posts arranged chronologiaclly for the current user.""" + + # Check for time and date filtered query params + status = request.GET.get("status") + + # If your posts + if status is None: + queryset = CommunityPost.objects.filter(thread_rank=1, posted_by=request.user + .profile).order_by("-time_of_modification") + else: + # If reported posts + if status == "3": + queryset = CommunityPost.objects.filter(status=status, deleted=False).order_by("-time_of_modification") + # queryset = CommunityPost.objects.all() + + else: + queryset = CommunityPost.objects.filter( + status=status, deleted=False, thread_rank=1).order_by("-time_of_modification") + queryset = query_search(request, 3, queryset, ['content'], 'posts') + queryset = query_from_num(request, 20, queryset) + + serializer = CommunityPostSerializerMin(queryset, many=True, context={'request': request}) + data = serializer.data + + return Response({'count': len(data), 'data': data}) + + @login_required_ajax + def create(self, request): + """Create Post and Comments. + Needs `AddP` permission for each body to be associated.""" + # Prevent posts without any community + if 'community' not in request.data or not request.data['community']: + return forbidden_no_privileges() + + return super().create(request) + + @login_required_ajax + def update(self, request, pk): + """Update Event. + Needs BodyRole with `UpdE` for at least one associated body. + Disassociating bodies from the event requires the `DelE` + permission and associating needs `AddE`""" + post = self.get_community_post(pk) + if post.posted_by != request.user.profile: + return forbidden_no_privileges() + + return super().update(request, pk) + + def perform_action(self, request, action, pk): + '''action==feature for featuring a post''' + post = self.get_community_post(pk) + + if action == "feature": + if all([user_has_privilege(request.user.profile, post.community.body.id, 'AppP')]): + + # Get query param + is_featured = request.data["is_featured"] + if is_featured is None: + return Response({"message": "{?is_featured} is required"}, status=400) + + # Check possible actions + + post.featured = is_featured + post.save() + return Response({"message": "is_featured changed", 'is_featured': is_featured}) + + return forbidden_no_privileges() + + if action == "delete": + if request.user.profile == post.posted_by: + post.deleted = True + post.featured = False + post.save() + return Response({"message": "Post deleted"}) + + if all([user_has_privilege(request.user.profile, post.community.body.id, 'AppP')]): + post.status = 2 + post.featured = False + post.save() + return Response({"message": "Post deleted"}) + + return forbidden_no_privileges() + + if action == "report": + if request.user.profile not in post.reported_by.all(): + post.reported_by.add(request.user.profile) + # post.reports +=1 + post.save() + return Response({"message": "Post reported"}) + post.reported_by.remove(request.user.profile) + # post.reports -=1 + post.save() + return Response({"message": "Post unreported"}) + + return Response({"message": "action not supported"}, status=400) + + def get_community_post(self, pk): + """Get a community post from pk uuid or strid.""" + try: + UUID(pk, version=4) + return get_object_or_404(self.queryset, id=pk) + except ValueError: + return get_object_or_404(self.queryset, str_id=pk) + +class CommunityViewSet(viewsets.ModelViewSet): + queryset = Community.objects + serializer_class = CommunitySerializers + + @login_required_ajax + def list(self, request): + queryset = Community.objects.all() + queryset = query_search(request, 3, queryset, ['name', 'about', 'description'], 'communities') + serializer = CommunitySerializerMin(queryset, many=True) + data = serializer.data + return Response(data) + + @login_required_ajax + def retrieve(self, request, pk): + # Prefetch and annotate data + self.queryset = CommunitySerializers.setup_eager_loading(self.queryset, request) + + # Try UUID or fall back to str_id + body = self.get_community(pk) + + # Serialize the body + serialized = CommunitySerializers(body, context={'request': request}).data + return Response(serialized) + + def get_community(self, pk): + try: + UUID(pk, version=4) + return get_object_or_404(self.queryset, id=pk) + except ValueError: + return get_object_or_404(self.queryset, str_id=pk) diff --git a/events/admin.py b/events/admin.py index f8d2b5ed..967dbd01 100644 --- a/events/admin.py +++ b/events/admin.py @@ -2,8 +2,8 @@ from events.models import Event, UserEventStatus class EventAdmin(admin.ModelAdmin): - list_filter = ('start_time',) - list_display = ('name', 'start_time', 'end_time') + list_filter = ('start_time', 'bodies',) + list_display = ('name', 'all_bodies', 'start_time', 'end_time',) search_fields = ['name'] ordering = ('-start_time',) raw_id_fields = ('created_by',) diff --git a/events/migrations/0030_event_event_interest.py b/events/migrations/0030_event_event_interest.py new file mode 100644 index 00000000..d756264d --- /dev/null +++ b/events/migrations/0030_event_event_interest.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.10 on 2021-12-31 06:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('achievements', '0012_auto_20211201_1642'), + ('events', '0029_auto_20210530_0055'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='event_interest', + field=models.ManyToManyField(blank=True, null=True, related_name='events', to='achievements.Interest'), + ), + ] diff --git a/events/migrations/0031_alter_event_event_interest.py b/events/migrations/0031_alter_event_event_interest.py new file mode 100644 index 00000000..915c01f2 --- /dev/null +++ b/events/migrations/0031_alter_event_event_interest.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.10 on 2022-01-14 06:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('achievements', '0012_auto_20211201_1642'), + ('events', '0030_event_event_interest'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='event_interest', + field=models.ManyToManyField(blank=True, related_name='events', to='achievements.Interest'), + ), + ] diff --git a/events/models.py b/events/models.py index 1242a779..3e4160fe 100644 --- a/events/models.py +++ b/events/models.py @@ -35,12 +35,21 @@ class Event(models.Model): starting_notified = models.BooleanField(default=False) + event_interest = models.ManyToManyField('achievements.Interest', related_name='events', + blank=True) + promotion_boost = models.IntegerField(default=0) weight = 0 def __str__(self): - return self.name + all_bodies = self.all_bodies() + if all_bodies == []: + return self.name + bodies_str = all_bodies[0] + for body in all_bodies[1:]: + bodies_str += ", " + body + return f"{self.name} - {bodies_str}" def save(self, *args, **kwargs): # pylint: disable=W0222 self.str_id = get_url_friendly(self.name) + "-" + str(self.id)[:8] @@ -49,6 +58,9 @@ def save(self, *args, **kwargs): # pylint: disable=W0222 def get_absolute_url(self): return '/event/' + self.str_id + def all_bodies(self): + return [str(body) for body in self.bodies.all()] + class Meta: verbose_name = "Event" verbose_name_plural = "Events" diff --git a/events/serializers.py b/events/serializers.py index 0ac22daa..9b9e4169 100644 --- a/events/serializers.py +++ b/events/serializers.py @@ -3,6 +3,8 @@ from django.db.models import Count from django.db.models import Prefetch from django.db.models import Q +from achievements.serializers import InterestSerializer +from achievements.models import Interest from events.models import Event from events.models import UserEventStatus from users.models import UserTag @@ -47,13 +49,14 @@ class EventSerializer(serializers.ModelSerializer): bodies = BodySerializerMin(many=True, read_only=True) offered_achievements = OfferedAchievementSerializer(many=True, read_only=True) + event_interest = InterestSerializer(many=True, read_only=True) class Meta: model = Event fields = ('id', 'str_id', 'name', 'description', 'image_url', 'start_time', 'end_time', 'all_day', 'venues', 'bodies', 'interested_count', 'going_count', 'website_url', 'weight', - 'user_ues', 'offered_achievements') + 'user_ues', 'offered_achievements', 'event_interest') @staticmethod def setup_eager_loading(queryset, request, extra_prefetch=None): @@ -97,11 +100,13 @@ class EventFullSerializer(serializers.ModelSerializer): interested_count = serializers.IntegerField(read_only=True) going_count = serializers.IntegerField(read_only=True) + def get_interested(self, obj): + return get_followers(obj, 1) interested = serializers.SerializerMethodField() - get_interested = lambda self, obj: get_followers(obj, 1) + def get_going(self, obj): + return get_followers(obj, 2) going = serializers.SerializerMethodField() - get_going = lambda self, obj: get_followers(obj, 2) user_ues = serializers.SerializerMethodField() get_user_ues = get_user_ues # pylint: disable=self-assigning-variable @@ -121,13 +126,18 @@ class EventFullSerializer(serializers.ModelSerializer): many=True, read_only=False, queryset=UserTag.objects.all(), default=[]) offered_achievements = OfferedAchievementSerializer(many=True, read_only=True) + event_interest = InterestSerializer(many=True, read_only=True) + + interests_id = serializers.PrimaryKeyRelatedField( + many=True, read_only=False, queryset=Interest.objects.all(), source='event_interest') class Meta: model = Event fields = ('id', 'str_id', 'name', 'description', 'image_url', 'start_time', 'end_time', 'all_day', 'venues', 'venue_names', 'bodies', 'bodies_id', 'interested_count', 'going_count', 'interested', 'going', 'venue_ids', - 'website_url', 'user_ues', 'notify', 'user_tags', 'offered_achievements') + 'website_url', 'user_ues', 'notify', 'user_tags', 'offered_achievements', + 'event_interest', 'interests_id') @staticmethod def setup_eager_loading(queryset, request): diff --git a/events/views.py b/events/views.py index 48ba7d63..131a0d42 100644 --- a/events/views.py +++ b/events/views.py @@ -68,6 +68,13 @@ def create(self, request): # Fill in ids of venues request.data['venue_ids'] = create_unreusable_locations(request.data['venue_names']) + try: + request.data['event_interest'] + request.data['interests_id'] + except KeyError: + request.data['event_interest'] = [] + request.data['interests_id'] = [] + return super().create(request) return forbidden_no_privileges() @@ -93,6 +100,13 @@ def update(self, request, pk): # Create added unreusable venues, unlink deleted ones request.data['venue_ids'] = get_update_venue_ids(request.data['venue_names'], event) + try: + request.data['event_interest'] + request.data['interests_id'] + except KeyError: + request.data['event_interest'] = [] + request.data['interests_id'] = [] + return super().update(request, pk) @login_required_ajax diff --git a/external/admin.py b/external/admin.py index a011cc5d..388355ce 100644 --- a/external/admin.py +++ b/external/admin.py @@ -1,4 +1,10 @@ from django.contrib import admin from external.models import ExternalBlogEntry -admin.site.register(ExternalBlogEntry) +class ExternalBlogAdmin(admin.ModelAdmin): + list_filter = ('published',) + list_display = ('title', 'body', 'published') + search_fields = ['title'] + + +admin.site.register(ExternalBlogEntry, ExternalBlogAdmin) diff --git a/external/management/commands/external_blog_chore.py b/external/management/commands/external_blog_chore.py index 5c330609..8252cba3 100644 --- a/external/management/commands/external_blog_chore.py +++ b/external/management/commands/external_blog_chore.py @@ -1,10 +1,13 @@ import re import feedparser import requests +from notifications.signals import notify from dateutil.parser import parse from django.conf import settings +from django.contrib.auth.models import User from django.core.management.base import BaseCommand, CommandError from external.models import ExternalBlogEntry +from bodies.models import Body from helpers.misc import table_to_markdown @@ -32,7 +35,7 @@ def handle_entry(entry): # Reuse if entry exists, create new otherwise if not db_entry: db_entry = ExternalBlogEntry(guid=guid) - # new_added = True + new_added = True # Fill the db entry if 'author' in entry: @@ -46,12 +49,15 @@ def handle_entry(entry): db_entry.save() - # # Send notification to mentioned people - # if new_added and db_entry.content: - # # Send notifications to followers - # if body is not None: - # users = User.objects.filter(id__in=body.followers.filter(active=True).values('user_id')) - # notify.send(db_entry, recipient=users, verb="New post on " + body.name) + # Finding the External Blog Body + body = Body.objects.filter(name="External Blog").first() + + # Send notification to mentioned people + if new_added and db_entry.content: + # Send notifications to followers + if body is not None: + users = User.objects.filter(id__in=body.followers.filter(active=True).values('user_id')) + notify.send(db_entry, recipient=users, verb="New post on " + body.name) # # Send notifications for mentioned users # roll_nos = [p for p in profile_fetcher.get_roll() if p and p in db_entry.content] @@ -62,7 +68,7 @@ def handle_entry(entry): def fill_blog(url): # Get the feed - response = requests.get(url) + response = requests.get(url, timeout=10) feeds = feedparser.parse(response.content) if not feeds['feed']: diff --git a/external/models.py b/external/models.py index e2cf74bf..8b2cf1ce 100644 --- a/external/models.py +++ b/external/models.py @@ -4,7 +4,7 @@ from django.utils.timezone import now class ExternalBlogEntry(models.Model): - """A single entry on the placements blog.""" + """A single entry on the external blog.""" id = models.UUIDField(primary_key=True, default=uuid4, editable=False) guid = models.CharField(max_length=200, blank=True) @@ -15,7 +15,7 @@ class ExternalBlogEntry(models.Model): body = models.TextField(max_length=50, blank=True) def __str__(self): - return self.title + return f"{self.title} - {self.body}" class Meta: verbose_name = "External Blog Entry" diff --git a/external/tests.py b/external/tests.py index a9b2212e..77ae4bd7 100644 --- a/external/tests.py +++ b/external/tests.py @@ -33,12 +33,12 @@ def setUp(self): ExternalBlogEntry.objects.create(title="TEntry2") ExternalBlogEntry.objects.create(title="TEntry3") - def test_placement_other(self): - """Check misc parameters of Placement.""" - self.assertEqual(str(self.entry1), self.entry1.title) + def test_external_other(self): + """Check misc parameters of External.""" + self.assertEqual(str(self.entry1), self.entry1.title + ' - ' + self.entry1.body) - def test_placement_get(self): - """Check auth before getting placements.""" + def test_external_get(self): + """Check auth before getting external blogs.""" test_blog(self, '/api/external-blog', 5) def test_blog_order(self): @@ -65,8 +65,8 @@ def test_blog_order(self): self.assertEqual(response.data[4]['id'], str(first_entry.id)) @freeze_time('2019-01-02') - def test_placements_chore(self): - """Test the placement blog chore.""" + def test_external_chore(self): + """Test the external blog chore.""" # Clear table ExternalBlogEntry.objects.all().delete() @@ -75,10 +75,10 @@ def test_placements_chore(self): call_command('external_blog_chore') # Check if posts were collected - placements = ExternalBlogEntry.objects.all() - self.assertEqual(placements.count(), 5) - self.assertEqual(set(x.guid for x in placements), set('sample:t:%i' % i for i in range(1, 6))) - self.assertEqual(set(x.title for x in placements), set('Training Item %i' % i for i in range(1, 6))) + externals = ExternalBlogEntry.objects.all() + self.assertEqual(externals.count(), 5) + self.assertEqual(set(x.guid for x in externals), set('sample:t:%i' % i for i in range(1, 6))) + self.assertEqual(set(x.title for x in externals), set('Training Item %i' % i for i in range(1, 6))) def tearDown(self): # Stop server diff --git a/helpers/test_helpers.py b/helpers/test_helpers.py index 3b8dcf04..c864ccd7 100644 --- a/helpers/test_helpers.py +++ b/helpers/test_helpers.py @@ -5,6 +5,7 @@ from events.models import Event from users.models import UserTag from users.models import UserTagCategory +from community.models import Community, CommunityPost def create_event(start_time_delta=0, end_time_delta=0, **kwargs): """Create an event with optional start and end times.""" @@ -28,6 +29,27 @@ def create_body(**kwargs): kwargs['name'] = 'TestBody%d' % create_body.i return Body.objects.create(**kwargs) +def create_community(**kwargs): + """Create a test body.""" + create_community.i += 1 + if 'name' not in kwargs: + kwargs['name'] = 'TestCommunity%d' % create_community.i + if 'body' not in kwargs: + kwargs['body'] = create_body() + return Community.objects.create(**kwargs) + +def create_communitypost(**kwargs): + """Create a test body.""" + if 'posted_by' not in kwargs: + raise Exception("Must specify posted_by") + + create_communitypost.i += 1 + if 'content' not in kwargs: + kwargs['content'] = 'TestCommunityPostContent%d' % create_communitypost.i + if 'community' not in kwargs: + kwargs['community'] = create_community() + return CommunityPost.objects.create(**kwargs) + def create_usertagcategory(name=None): """Create a test tag category.""" create_usertagcategory.i += 1 @@ -44,5 +66,7 @@ def create_usertag(category, regex, target='hostel', name='tag', **kwargs): create_event.i = 0 create_body.i = 0 +create_community.i = 0 +create_communitypost.i = 0 create_usertagcategory.i = 0 create_usertag.i = 0 diff --git a/helpers/views.py b/helpers/views.py new file mode 100644 index 00000000..e69de29b diff --git a/locations/management/commands/krishna.py b/locations/management/commands/krishna.py new file mode 100644 index 00000000..391c3402 --- /dev/null +++ b/locations/management/commands/krishna.py @@ -0,0 +1,70 @@ +import re +import feedparser +import requests +from notifications.signals import notify +from dateutil.parser import parse +from django.conf import settings +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand, CommandError +from external.models import ExternalBlogEntry +from bodies.models import Body +from helpers.misc import table_to_markdown + +from locations.models import Location, LocationLocationDistance + + +# class ProfileFetcher(): +# """Helper to get dictionary of profiles efficiently.""" +# def __init__(self): +# self.roll_nos = None + +# def get_roll(self): +# if not self.roll_nos: +# self.roll_nos = UserProfile.objects.filter(active=True).values_list('roll_no', flat=True) +# return self.roll_nos + + +# profile_fetcher = ProfileFetcher() + +def handle_entry(entry): + coordinates = [(4228, 943), (4005, 899), (2100, 756)] + adj_list = { + 0: {1: -1, 2: -1}, + 1: {0: -1}, + 2: {0: -1}, + } + for x in adj_list: + for y in adj_list[x]: + adj_list[x][y] = abs(0.001 * ((coordinates[x][0] ^ 2) - (coordinates[y][0] ^ 2) + + (coordinates[x][1] ^ 2) - (coordinates[y][1] ^ 2))) + + i = 0 + loc_list = [] + for p in coordinates: + loc, c = Location.objects.get_or_create(pixel_x=p[0], pixel_y=p[1], name="Node" + str(i)) + loc_list.append(loc) + i += 1 + + for i in range(0, len(coordinates)): + loc1 = loc_list[i] + print(adj_list[i]) + for loc2_ind in adj_list[i]: + loc2 = loc_list[loc2_ind] + dist = adj_list[i][loc2_ind] + lld = LocationLocationDistance.objects.filter(location1__id=loc1.id, location2__id=loc2.id).first() + if not lld: + LocationLocationDistance.objects.create( + location1=loc1, location2=loc2, distance=dist) + else: + lld.distance = dist + lld.save() + + +class Command(BaseCommand): + help = 'Updates the external blog database' + + def handle(self, *args, **options): + """Run the chore.""" + + handle_entry(settings.EXTERNAL_BLOG_URL) + self.stdout.write(self.style.SUCCESS('External Blog Chore completed successfully')) diff --git a/locations/migrations/0015_auto_20230111_1809.py b/locations/migrations/0015_auto_20230111_1809.py new file mode 100644 index 00000000..816b9515 --- /dev/null +++ b/locations/migrations/0015_auto_20230111_1809.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.10 on 2023-01-11 12:39 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('locations', '0014_location_str_id'), + ] + + operations = [ + migrations.CreateModel( + name='LocationLocationDistance', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('distance', models.FloatField(default=100000000)), + ('location1', models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='lld1', to='locations.location')), + ('location2', models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='lld2', to='locations.location')), + ], + options={ + 'verbose_name': 'Location-Location Distance', + 'verbose_name_plural': 'Location-Location Distances', + }, + ), + migrations.AddField( + model_name='location', + name='adjacent_locs', + field=models.ManyToManyField(blank=True, related_name='adjacent_loc', through='locations.LocationLocationDistance', to='locations.Location'), + ), + ] diff --git a/locations/models.py b/locations/models.py index 4aba9d92..6061ac90 100644 --- a/locations/models.py +++ b/locations/models.py @@ -28,6 +28,10 @@ class Location(models.Model): lng = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) reusable = models.BooleanField(default=False) + adjacent_locs = models.ManyToManyField( + 'locations.Location', through='LocationLocationDistance', + related_name='adjacent_loc', blank=True) + def save(self, *args, **kwargs): # pylint: disable=W0222 self.str_id = get_url_friendly(self.short_name) super().save(*args, **kwargs) @@ -43,3 +47,16 @@ class Meta: models.Index(fields=['reusable', ]), models.Index(fields=['reusable', 'group_id']), ] + +class LocationLocationDistance(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + + location1 = models.ForeignKey(Location, on_delete=models.CASCADE, + default=uuid4, related_name='lld1') + location2 = models.ForeignKey(Location, on_delete=models.CASCADE, + default=uuid4, related_name='lld2') + distance = models.FloatField(default=100000000) + + class Meta: + verbose_name = "Location-Location Distance" + verbose_name_plural = "Location-Location Distances" diff --git a/login/helpers.py b/login/helpers.py index 255a091c..2ba2712d 100644 --- a/login/helpers.py +++ b/login/helpers.py @@ -28,7 +28,7 @@ def perform_login(auth_code, redir, request): headers={ "Authorization": "Basic " + settings.SSO_CLIENT_ID_SECRET_BASE64, "Content-Type": "application/x-www-form-urlencoded" - }, verify=not settings.SSO_BAD_CERT) + }, verify=not settings.SSO_BAD_CERT, timeout=10) response_json = response.json() # Check that we have the access token @@ -40,7 +40,7 @@ def perform_login(auth_code, redir, request): settings.SSO_PROFILE_URL, headers={ "Authorization": "Bearer " + response_json['access_token'], - }, verify=not settings.SSO_BAD_CERT) + }, verify=not settings.SSO_BAD_CERT, timeout=10) profile_json = profile_response.json() # Check if we got at least the user's SSO id @@ -221,3 +221,10 @@ def perform_alumni_login(request, ldap_entered): user = UserProfile.objects.filter(query).first().user login(request, user) request.session.save() + queryset = UserProfileFullSerializer.setup_eager_loading(UserProfile.objects) + user_profile = queryset.get(user=user) + return request.session.session_key, user.username, user_profile.id, UserProfileFullSerializer( + user_profile, context={ + 'request': request + } + ).data diff --git a/login/views.py b/login/views.py index 10ded938..8287a5e2 100644 --- a/login/views.py +++ b/login/views.py @@ -162,8 +162,15 @@ def alumni_otp_conf(request): # Check key if lastRequest.isCorrectKey(key): # Perform login - perform_alumni_login(request, ldap_entered) - return Response({'error_status': False, 'msg': 'Logged in'}) + session_key, user, profile_id, profile = perform_alumni_login(request, ldap_entered) + return Response({ + 'error_status': False, + 'msg': 'Logged in', + 'sessionid': session_key, + 'user': user, + 'profile_id': profile_id, + 'profile': profile + }) return Response({'error_status': True, 'msg': 'Wrong OTP, retry'}) diff --git a/messmenu/admin.py b/messmenu/admin.py index 755d8749..2403dd6c 100644 --- a/messmenu/admin.py +++ b/messmenu/admin.py @@ -1,7 +1,18 @@ from django.contrib import admin -from messmenu.models import Hostel +from messmenu.models import Hostel, MessCalEvent from messmenu.models import MenuEntry +class HostelAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ['name'] + + +class MessCalEventAdmin(admin.ModelAdmin): + list_filter = ('datetime', 'hostel',) + list_display = ('user', 'hostel', 'title', 'datetime',) + + # Register your models here. -admin.site.register(Hostel) +admin.site.register(Hostel, HostelAdmin) admin.site.register(MenuEntry) +admin.site.register(MessCalEvent, MessCalEventAdmin) diff --git a/messmenu/migrations/0004_messcalevent.py b/messmenu/migrations/0004_messcalevent.py new file mode 100644 index 00000000..971b64a9 --- /dev/null +++ b/messmenu/migrations/0004_messcalevent.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.10 on 2022-01-14 06:50 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0038_auto_20210606_2237'), + ('messmenu', '0003_hostel_long_name'), + ] + + operations = [ + migrations.CreateModel( + name='MessCalEvent', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('hostel', models.IntegerField()), + ('datetime', models.DateTimeField()), + ('title', models.CharField(max_length=100)), + ('user', models.ForeignKey(default=uuid.uuid4, on_delete=django.db.models.deletion.CASCADE, related_name='ums', to='users.userprofile')), + ], + ), + ] diff --git a/messmenu/models.py b/messmenu/models.py index 7db3273c..6ce8f011 100644 --- a/messmenu/models.py +++ b/messmenu/models.py @@ -31,3 +31,18 @@ class MenuEntry(models.Model): def __str__(self): return self.hostel.name + ' - ' + str(self.day) + +class MessCalEvent(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + hostel = models.IntegerField() + datetime = models.DateTimeField() + title = models.CharField(max_length=100) # Breakfast, Lunch, Snacks, Dinner + user = models.ForeignKey( + 'users.UserProfile', + on_delete=models.CASCADE, + default=uuid4, + related_name='ums' + ) + + def __str__(self): + return f"{str(self.user)}, {self.title}" diff --git a/messmenu/serializers.py b/messmenu/serializers.py index 8e298d55..fe6893fd 100644 --- a/messmenu/serializers.py +++ b/messmenu/serializers.py @@ -1,6 +1,6 @@ """Serializers for mess menu.""" from rest_framework import serializers -from messmenu.models import MenuEntry +from messmenu.models import MenuEntry, MessCalEvent from messmenu.models import Hostel class MenuEntrySerializer(serializers.ModelSerializer): @@ -22,3 +22,9 @@ def setup_eager_loading(queryset): """Perform necessary eager loading of data.""" queryset = queryset.prefetch_related('mess') return queryset + +class MessCalEventSerializer(serializers.ModelSerializer): + """Serializer for mess calendar event.""" + class Meta: + model = MessCalEvent + fields = '__all__' diff --git a/messmenu/urls.py b/messmenu/urls.py index 95d6a640..d5d8f518 100644 --- a/messmenu/urls.py +++ b/messmenu/urls.py @@ -1,7 +1,9 @@ """URLs for mess menu.""" from django.urls import path -from messmenu.views import get_mess +from messmenu.views import get_mess, getUserMess, getRnoQR urlpatterns = [ path('mess', get_mess), + path('getUserMess', getUserMess), + path('getEncr', getRnoQR), ] diff --git a/messmenu/views.py b/messmenu/views.py index 52c99f11..e7646f83 100644 --- a/messmenu/views.py +++ b/messmenu/views.py @@ -1,8 +1,14 @@ """Views for mess menu.""" +from datetime import datetime +import requests from rest_framework.response import Response from rest_framework.decorators import api_view -from messmenu.models import Hostel -from messmenu.serializers import HostelSerializer +from dateutil.relativedelta import relativedelta +from django.conf import settings +from django.utils.timezone import make_aware +from cryptography.fernet import Fernet +from messmenu.models import Hostel, MessCalEvent +from messmenu.serializers import HostelSerializer, MessCalEventSerializer @api_view(['GET', ]) def get_mess(request): @@ -10,3 +16,122 @@ def get_mess(request): queryset = Hostel.objects.all() queryset = HostelSerializer.setup_eager_loading(queryset) return Response(HostelSerializer(queryset, many=True).data) + +@api_view(['GET', ]) +def getUserMess(request): + """Get mess status for a user""" + + try: + request.user.profile + except AttributeError: + return Response({ + 'message': 'unauthenticated', + 'detail': 'Log in to continue!' + }, status=401) + + user = request.user.profile + rollno = user.roll_no + + start = request.GET.get('start') + end = request.GET.get('end') + + start = datetime.strptime(start, '%Y-%m-%d %H:%M:%S') + end = datetime.strptime(end, '%Y-%m-%d %H:%M:%S') + + curr = start + + items = [] + + while curr <= end: + new_items = getMessForMonth(user, rollno, curr) + if new_items is not None: + items.extend(new_items) + curr = curr + relativedelta(months=1) + + return Response(MessCalEventSerializer(items, many=True).data) + + +def binaryDecode(x): + b_x = "{0:b}".format(int(x)) + day = int(b_x[len(b_x) - 5:len(b_x)], 2) + meal = b_x[len(b_x) - 8:len(b_x) - 5] + time = int(b_x[len(b_x) - 19:len(b_x) - 8], 2) + hostel = int(b_x[0:len(b_x) - 19], 2) + return {'hostel': hostel, 'time': time, 'meal': meal, 'day': day} + +def getMessForMonth(user, rollno, curr): + items = [] + url = f'{settings.MESSI_BASE_URL}/api/get_details?roll={rollno}&year={curr.year}&month={curr.month}' + payload = {} + headers = { + 'x-access-token': settings.MESSI_ACCESS_TOKEN + } + + res = requests.request("GET", url, headers=headers, data=payload, timeout=10) + + if res.status_code != 200: + return None + # print("Error in getting details") + # return Response({"error":"Error in getting mess calendar"}) + + data = res.json() + + try: + details = data["details"] + + for d in details: + k = binaryDecode(d) + mealnum = k["meal"] + + title = { + "000": "Breakfast", + "001": "Lunch", + "010": "Snacks", + "011": "Dinner", + "100": "Milk", + "101": "Egg", + "110": "Fruit" + }.get(mealnum, "Other") + + date = datetime(curr.year, curr.month, k["day"], k["time"] // 60, k["time"] % 60) + date = make_aware(date) + hostel = k["hostel"] + + item, c = MessCalEvent.objects.get_or_create(user=user, datetime=date, hostel=hostel) + if c or item.title != title: + item.title = title + item.save() + + items.append(item) + + except KeyError: + return None + + return items + +@api_view(['GET', ]) +def getRnoQR(request): + + try: + request.user.profile + except AttributeError: + return Response({ + 'message': 'unauthenticated', + 'detail': 'Log in to continue!' + }, status=401) + + try: + user = request.user.profile + rollno = (str(user.roll_no)).upper() + # rollno = "200020087" + time = str(datetime.now()) + rnom = (rollno + "," + time).encode() + + f = Fernet(b'Tolm_fRDkfoN5WMU4oUXWxNwmn1E0MmYlbeh1LA29cU=') + encrRno = f.encrypt(rnom) + + return Response({"qrstring": encrRno}) + except Exception as e: + return Response({ + 'qrstring': str(e), + }) diff --git a/news/management/commands/news_chore.py b/news/management/commands/news_chore.py index 006bcc66..c1790c1c 100644 --- a/news/management/commands/news_chore.py +++ b/news/management/commands/news_chore.py @@ -13,7 +13,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def fill_blog(url, body): - response = requests.get(url, verify=False) + response = requests.get(url, verify=False, timeout=10) feeds = feedparser.parse(response.content) if not feeds['feed']: diff --git a/news/models.py b/news/models.py index 9c142034..cd87943d 100644 --- a/news/models.py +++ b/news/models.py @@ -6,7 +6,6 @@ class NewsEntry(models.Model): """A single entry on a news blog.""" - id = models.UUIDField(primary_key=True, default=uuid4, editable=False) body = models.ForeignKey(Body, on_delete=models.CASCADE) guid = models.CharField(max_length=200, blank=True) diff --git a/other/management/commands/clean-notifications.py b/other/management/commands/clean-notifications.py index e0e86028..a6e6d1b9 100644 --- a/other/management/commands/clean-notifications.py +++ b/other/management/commands/clean-notifications.py @@ -17,7 +17,7 @@ def handle(self, *args, **options): queryset = Notification.objects.filter(timestamp__lte=now() - timedelta(days=90)) print('Dumping and Cleaning up %s old notifications' % queryset.count()) if queryset.count() > 0: - notif_list = list(queryset.values()) + notif_list = queryset.values() with open(file_path, 'a') as f: for notif in notif_list: json.dump(notif, f, ensure_ascii=False, default=str) diff --git a/other/management/commands/refresh-devices.py b/other/management/commands/refresh-devices.py index c14bbe96..7095ff31 100644 --- a/other/management/commands/refresh-devices.py +++ b/other/management/commands/refresh-devices.py @@ -11,12 +11,12 @@ class Command(BaseCommand): def handle(self, *args, **options): # Initiate connection push_service = FCMNotification(api_key=settings.FCM_SERVER_KEY) - + print("Refreshing Devices") # Refresh all for device in Device.objects.all(): - print(device.user.name + ' - ', end='', flush=True) if fill_device_firebase(push_service, device): - print('OK') + pass else: device.delete() + print(device.user.name + ' - ', end='', flush=True) print('FAIL') diff --git a/other/models.py b/other/models.py index 59e838f6..d4cbeff3 100644 --- a/other/models.py +++ b/other/models.py @@ -31,8 +31,9 @@ def supports_rich(self): # Check for flutter and iOS if self.application == 'app.insti.flutter': - return False - if self.application == 'app.insti.ios': + return int(self.app_version) >= 24 + + if self.application == 'app.instiapp.flutter': return False # Try parsing the app version diff --git a/other/notifications.py b/other/notifications.py index 9cd2ccde..a9287e53 100644 --- a/other/notifications.py +++ b/other/notifications.py @@ -4,13 +4,13 @@ from django.contrib.auth.models import User from django.db.models.signals import post_save from django.db.models.signals import m2m_changed +from community.models import CommunityPost, CommunityPostUserReaction import other.tasks as tasks from events.models import Event from news.models import NewsEntry from venter.models import ComplaintComment # pylint: disable=W0223,W0613 - def notify_new_event(instance, action, **kwargs): """Notify users that a new event was added for a followed body.""" if action == 'post_add' and isinstance(instance, Event): @@ -30,6 +30,23 @@ def notify_upd_event(instance): # Notify all event followers tasks.notify_upd_event.delay(instance.id) +def notify_new_commpost(instance, created, **kwargs): + """Notify users that a new post was created.""" + if isinstance(instance, CommunityPost): + if (instance.thread_rank == 1 and instance.status == 1 and not instance.deleted): + # Notify all body followers + tasks.notify_new_commpost.delay(instance.id) + elif (instance.thread_rank > 1 and instance.status == 1 and not instance.deleted): + tasks.notify_new_comm.delay(instance.id) + elif (instance.thread_rank == 1 and instance.status == 0 and not instance.deleted): + # Notify all body followers + tasks.notify_new_commpostadmin.delay(instance.id) + +def notify_new_reaction(instance, created, **kwargs): + """Notify users a new reaction on their post""" + if isinstance(instance, CommunityPostUserReaction) and created: + tasks.notify_new_reaction.delay(instance.id) + def event_saved(instance, created, **kwargs): """Notify users when an event changes.""" if not created: @@ -61,3 +78,6 @@ def notification_saved(instance, created, **kwargs): post_save.connect(new_comment, sender=ComplaintComment) post_save.connect(notification_saved, sender=Notification) +post_save.connect(notify_new_commpost, sender=CommunityPost) + +post_save.connect(notify_new_reaction, sender=CommunityPostUserReaction) diff --git a/other/search.py b/other/search.py index aa5b9160..8557e0d8 100644 --- a/other/search.py +++ b/other/search.py @@ -90,9 +90,9 @@ def space(*args): if typ == 'BlogEntry': val = space(obj.title, obj.content) - if obj.blog_url == settings.TRAINING_BLOG_URL: + if obj.blog_url == settings.TRAINING_BLOG_URL_VAL: return ('training', bucket, str(obj.id), val) - if obj.blog_url == settings.PLACEMENTS_URL: + if obj.blog_url == settings.PLACEMENTS_URL_VAL: return ('placement', bucket, str(obj.id), val) return ('blogs', bucket, str(obj.id), val) diff --git a/other/serializers.py b/other/serializers.py index 3092fb72..4209ff8d 100644 --- a/other/serializers.py +++ b/other/serializers.py @@ -1,21 +1,26 @@ """Serializers for non-specific models.""" from rest_framework import serializers +from community.models import CommunityPost, Community, CommunityPostUserReaction +from community.serializers import CommunityPostSerializers, CommunitySerializers from events.models import Event from events.serializers import EventSerializer +from external.models import ExternalBlogEntry from placements.models import BlogEntry from placements.serializers import BlogEntrySerializer from news.models import NewsEntry from news.serializers import NewsEntrySerializer from venter.models import ComplaintComment from venter.serializers import CommentSerializer -from users.models import UserTag -from users.models import UserTagCategory +from users.models import UserTag, UserTagCategory, UserProfile +from users.serializers import UserProfileSerializer from querybot.models import UnresolvedQuery from querybot.serializers import UnresolvedQuerySerializer class GenericNotificationRelatedField(serializers.RelatedField): # pylint: disable=W0223 """Serializer for actor/target of notifications.""" + def to_representation(self, value): + serializer = None if isinstance(value, Event): serializer = EventSerializer(value) elif isinstance(value, NewsEntry): @@ -26,8 +31,19 @@ def to_representation(self, value): serializer = CommentSerializer(value) elif isinstance(value, UnresolvedQuery): serializer = UnresolvedQuerySerializer(value) - - return serializer.data + elif isinstance(value, ExternalBlogEntry): + serializer = ExternalBlogEntry(value) + elif isinstance(value, CommunityPost): + serializer = CommunityPostSerializers(value) + elif isinstance(value, Community): + serializer = CommunitySerializers(value) + elif isinstance(value, CommunityPostUserReaction): + serializer = CommunityPostSerializers(value.communitypost) + elif isinstance(value, UserProfile): + serializer = UserProfileSerializer(value) + if serializer: + return serializer.data + return None class NotificationSerializer(serializers.Serializer): # pylint: disable=W0223 """Notification Serializer, with unread and actor""" diff --git a/other/tasks.py b/other/tasks.py index a0eb54ca..b5f58234 100644 --- a/other/tasks.py +++ b/other/tasks.py @@ -5,6 +5,8 @@ from notifications.models import Notification from notifications.signals import notify from pyfcm import FCMNotification +from achievements.models import UserInterest +from community.models import CommunityPost, CommunityPostUserReaction from events.models import Event from helpers.celery import shared_task_conditional from helpers.celery import FaultTolerantTask @@ -36,6 +38,16 @@ def notify_new_event(pk): verb=body.name + " has added a new event" ) + for interest in instance.event_interest.all(): + users = User.objects.filter( + id__in=UserInterest.filter(title=interest.title).user.filter(active=True).values('user_id') + ) + notify.send( + instance, + recipient=users, + verb=f"A new event with tag {interest.title} has been added" + ) + @shared_task_conditional(base=FaultTolerantTask) def notify_upd_event(pk): """Notify users about event updation.""" @@ -48,6 +60,88 @@ def notify_upd_event(pk): notify.send(instance, recipient=users, verb=instance.name + " was updated") @shared_task_conditional(base=FaultTolerantTask) +def notify_new_commpost(pk): + """Notify users about post creation.""" + setUp() + instance = CommunityPost.objects.filter(id=pk).first() + if not instance: + return + + community = instance.community + body = community.body + users = User.objects.filter(id__in=body.followers.filter(active=True).values('user_id')) + notify.send( + instance, + recipient=users, + verb="New post added in " + community.name + ) + + for interest in instance.interests.all(): + users = User.objects.filter( + id__in=UserInterest.filter(title=interest.title).user.filter(active=True).values('user_id') + ) + notify.send( + instance, + recipient=users, + verb=f"New post with tag {interest.title} added in {community.name}" + ) + +@shared_task_conditional(base=FaultTolerantTask) +def notify_new_comm(pk): + """Notify users about event creation.""" + setUp() + instance = CommunityPost.objects.filter(id=pk).first() + if not instance: + return + + commented_user = instance.posted_by + users = [] + while instance.thread_rank > 1: + instance = instance.parent + users.append(instance.posted_by.user) + notify.send( + instance, + recipient=users, + verb=commented_user.name + " commented to your post " + instance.content) + +@shared_task_conditional(base=FaultTolerantTask) +def notify_new_commpostadmin(pk): + """Notify users about event creation.""" + setUp() + instance = CommunityPost.objects.filter(id=pk).first() + if not instance: + return + + community = instance.community + roles = instance.community.body.roles.all() + users = [] + for role in roles: + if "AppP" in role.permissions: + users.extend(map(lambda user: user.user, role.users.all())) + print(users) + notify.send( + instance, + recipient=users, + verb="New post added for verification in " + community.name) + + +@ shared_task_conditional(base=FaultTolerantTask) +def notify_new_reaction(pk): + """Notify user about new reaction to his post/comment""" + setUp() + instance = CommunityPostUserReaction.objects.filter(id=pk).first() + if not instance: + return + + user = [instance.communitypost.posted_by.user] + notify.send( + instance, + recipient=user, + verb=instance.user.name + " reacted to you post " + instance.communitypost.content + ) + + +@ shared_task_conditional(base=FaultTolerantTask) def push_notify(pk): """Push notify a notification.""" setUp() @@ -75,7 +169,10 @@ def push_notify(pk): if not hasattr(settings, 'FCM_SERVER_KEY'): return - push_service = FCMNotification(api_key=settings.FCM_SERVER_KEY) + try: + push_service = FCMNotification(api_key=settings.FCM_SERVER_KEY) + except Exception: + return # Send FCM push notification for device in profile.devices.all(): diff --git a/other/tests.py b/other/tests.py index 2516b957..5e96a24b 100644 --- a/other/tests.py +++ b/other/tests.py @@ -134,9 +134,9 @@ def test_search_misc(self): self.assertEqual(index_pair(ent)[0], 'external') # Test indexing of PT blog - ent = BlogEntry(title='strategy comp', blog_url=settings.PLACEMENTS_URL) + ent = BlogEntry(title='strategy comp', blog_url=settings.PLACEMENTS_URL_VAL) self.assertEqual(index_pair(ent)[0], 'placement') - ent = BlogEntry(title='ecomm comp', blog_url=settings.TRAINING_BLOG_URL) + ent = BlogEntry(title='ecomm comp', blog_url=settings.TRAINING_BLOG_URL_VAL) self.assertEqual(index_pair(ent)[0], 'training') ent = BlogEntry(title='why this', blog_url='https://google.com') self.assertEqual(index_pair(ent)[0], 'blogs') @@ -190,8 +190,9 @@ def test_notifications(self): # pylint: disable=R0914,R0915 self.assertIn(EventSerializer(event3).data, actors) # Mark event2 as read + def e2notif(): + return Notification.objects.get(pk=e2n['id']) e2n = [n for n in response.data if n['actor'] == EventSerializer(event2).data][0] - e2notif = lambda: Notification.objects.get(pk=e2n['id']) self.assertEqual(e2notif().unread, True) self.assertEqual(e2notif().deleted, False) response = self.client.get(url + '/read/' + str(e2n['id'])) diff --git a/other/views.py b/other/views.py index 8ef5af81..b2fe047d 100644 --- a/other/views.py +++ b/other/views.py @@ -49,7 +49,7 @@ def search(request): # Search bodies by name and description if 'bodies' in types: bodies = query_search( - request, MIN_LENGTH, Body.objects, ['name', 'description'], + request, MIN_LENGTH, Body.objects, ['name', 'canonical_name', 'description'], 'bodies', order_relevance=True) # Search events by name and description @@ -73,7 +73,7 @@ def search(request): if 'interests' in types: interests = query_search( request, 0, Interest.objects.all(), - ["title"], 'interests', order_relevance=True)[:20] + ["title"], 'interests', order_relevance=True) return Response({ "bodies": BodySerializerMin(bodies, many=True).data, diff --git a/placements/admin.py b/placements/admin.py index adeb676f..4760e23c 100644 --- a/placements/admin.py +++ b/placements/admin.py @@ -1,4 +1,10 @@ from django.contrib import admin from placements.models import BlogEntry -admin.site.register(BlogEntry) +class BlogEntryAdmin(admin.ModelAdmin): + list_filter = ('published',) + list_display = ('title', 'published') + search_fields = ['title'] + + +admin.site.register(BlogEntry, BlogEntryAdmin) diff --git a/placements/management/commands/placement_blog_chore.py b/placements/management/commands/placement_blog_chore.py index d9be266e..6915d5a2 100644 --- a/placements/management/commands/placement_blog_chore.py +++ b/placements/management/commands/placement_blog_chore.py @@ -1,7 +1,11 @@ +import pickle import re +from os.path import exists +from requests.auth import HTTPBasicAuth +# from urllib import response import feedparser import requests -from requests.auth import HTTPBasicAuth +# from requests.auth import HTTPBasicAuth from dateutil.parser import parse from django.conf import settings from django.contrib.auth.models import User @@ -14,6 +18,7 @@ class ProfileFetcher(): """Helper to get dictionary of profiles efficiently.""" + def __init__(self): self.roll_nos = None @@ -63,24 +68,51 @@ def handle_entry(entry, body, url): users = User.objects.filter(profile__roll_no__in=roll_nos) notify.send(db_entry, recipient=users, verb="You were mentioned in a blog post") -def fill_blog(url, body_name): +def fill_blog(url, body_name, url_val): # Get the body body = Body.objects.filter(name=body_name).first() - # Get the feed - response = requests.get(url, auth=HTTPBasicAuth( - settings.LDAP_USERNAME, settings.LDAP_PASSWORD)) - feeds = feedparser.parse(response.content) + if exists('session.obj'): + f = open('session.obj', 'rb') + s = pickle.load(f) + f.close() + + # Get the feed + # response = requests.get(url, auth=HTTPBasicAuth( + # settings.LDAP_USERNAME, settings.LDAP_PASSWORD)) + + response = s.get(url, verify=False, timeout=10) + + feeds = feedparser.parse(response.content) + + if not feeds['feed']: + raise CommandError('PLACEMENTS BLOG CHORE FAILED') + + # Add each entry if doesn't exist + for entry in feeds['entries']: + handle_entry(entry, body, url_val) + + else: + # Get the body + body = Body.objects.filter(name=body_name).first() + + # Get the feed + response = requests.get(url, auth=HTTPBasicAuth( + settings.LDAP_USERNAME, settings.LDAP_PASSWORD), timeout=10) + feeds = feedparser.parse(response.content) + + if not feeds['feed']: + raise CommandError('PLACEMENTS BLOG CHORE FAILED') - if not feeds['feed']: - raise CommandError('PLACEMENTS BLOG CHORE FAILED') + # Add each entry if doesn't exist + for entry in feeds['entries']: + handle_entry(entry, body, url) - # Add each entry if doesn't exist - for entry in feeds['entries']: - handle_entry(entry, body, url) def handle_html(content): # Convert tables to markdown + figregex = re.compile(r"
", re.DOTALL) + content = figregex.sub(convert_table_md, content) regex = re.compile(r"", re.DOTALL) content = regex.sub(convert_table_md, content) return content @@ -96,7 +128,7 @@ class Command(BaseCommand): def handle(self, *args, **options): """Run the chore.""" - fill_blog(settings.PLACEMENTS_URL, settings.PLACEMENTS_BLOG_BODY) - fill_blog(settings.TRAINING_BLOG_URL, settings.TRAINING_BLOG_BODY) + fill_blog(settings.PLACEMENTS_URL, settings.PLACEMENTS_BLOG_BODY, settings.PLACEMENTS_URL_VAL) + fill_blog(settings.TRAINING_BLOG_URL, settings.TRAINING_BLOG_BODY, settings.TRAINING_BLOG_URL_VAL) self.stdout.write(self.style.SUCCESS('Placement Blog Chore completed successfully')) diff --git a/placements/management/commands/sso_login_chore.py b/placements/management/commands/sso_login_chore.py new file mode 100644 index 00000000..944aac5c --- /dev/null +++ b/placements/management/commands/sso_login_chore.py @@ -0,0 +1,32 @@ +import pickle +from requests import Session +from django.core.management.base import BaseCommand, CommandError +from django.conf import settings +import pyotp + +class Command(BaseCommand): + help = 'Updates the placement blog database' + + def handle(self, *args, **options): + # """Run the chore.""" + + s = Session() + + init_response = s.get('https://sso.iitb.ac.in/login') + + if not settings.LDAP_USERNAME or not settings.LDAP_PASSWORD: + raise CommandError('SSO LOGIN CHORE FAILED: NO CREDENTIALS PROVIDED') + + s.post('https://sso.iitb.ac.in/login', cookies=init_response.cookies, data={ + 'username': settings.LDAP_USERNAME, + 'password': settings.LDAP_PASSWORD, + 'remember': 'on', + 'redir': '/', + 'totp': int(pyotp.TOTP(settings.SSO_TOTP_TOKEN).now()) + }, allow_redirects=True) + + f = open('session.obj', 'wb') + pickle.dump(obj=s, file=f) + f.close() + + self.stdout.write(self.style.SUCCESS('SSO Login Chore completed successfully')) diff --git a/placements/tests.py b/placements/tests.py index c2b28e35..1dad7777 100644 --- a/placements/tests.py +++ b/placements/tests.py @@ -31,11 +31,11 @@ def setUp(self): self.mock_server = Popen(['python', 'news/test/test_server.py']) # Create dummies - self.entry1 = BlogEntry.objects.create(title="PEntry1", blog_url=settings.PLACEMENTS_URL) - BlogEntry.objects.create(title="PEntry2", blog_url=settings.PLACEMENTS_URL) - BlogEntry.objects.create(title="TEntry1", blog_url=settings.TRAINING_BLOG_URL) - BlogEntry.objects.create(title="TEntry2", blog_url=settings.TRAINING_BLOG_URL) - BlogEntry.objects.create(title="TEntry3", blog_url=settings.TRAINING_BLOG_URL) + self.entry1 = BlogEntry.objects.create(title="PEntry1", blog_url=settings.PLACEMENTS_URL_VAL) + BlogEntry.objects.create(title="PEntry2", blog_url=settings.PLACEMENTS_URL_VAL) + BlogEntry.objects.create(title="TEntry1", blog_url=settings.TRAINING_BLOG_URL_VAL) + BlogEntry.objects.create(title="TEntry2", blog_url=settings.TRAINING_BLOG_URL_VAL) + BlogEntry.objects.create(title="TEntry3", blog_url=settings.TRAINING_BLOG_URL_VAL) def test_placement_other(self): """Check misc parameters of Placement.""" @@ -51,24 +51,25 @@ def test_training_get(self): # Adding test for pin_unpin feature - def test_blog_order(self): - """Test ordering of the pinned blogs""" - BlogEntry.objects.create(title="UnpinnedEntry2", blog_url=settings.PLACEMENTS_URL,) - pinnedEntry1 = BlogEntry.objects.create(title="PinnedEntry1", blog_url=settings.PLACEMENTS_URL, pinned=True) + # def test_blog_order(self): + # """Test ordering of the pinned blogs""" + # BlogEntry.objects.create(title="UnpinnedEntry2", blog_url=settings.PLACEMENTS_URL_VAL,) + # pinnedEntry1 = BlogEntry.objects.create(title="PinnedEntry1", + # blog_url=settings.PLACEMENTS_URL_VAL, pinned=True) - BlogEntry.objects.create(title="UnpinnedEntry3", blog_url=settings.PLACEMENTS_URL,) - BlogEntry.objects.create(title="UnpinnedEntry4", blog_url=settings.PLACEMENTS_URL,) + # BlogEntry.objects.create(title="UnpinnedEntry3", blog_url=settings.PLACEMENTS_URL_VAL,) + # BlogEntry.objects.create(title="UnpinnedEntry4", blog_url=settings.PLACEMENTS_URL_VAL,) - user = get_new_user() - self.client.force_authenticate(user) # pylint: disable=E1101 + # user = get_new_user() + # self.client.force_authenticate(user) # pylint: disable=E1101 - url = '/api/placement-blog' - response = self.client.get(url) + # url = '/api/placement-blog' + # response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data[0]['id'], str(pinnedEntry1.id)) - self.assertEqual(response.data[0]['pinned'], True) - # to test that the latest blog appears just below the pinned one + # self.assertEqual(response.status_code, 200) + # self.assertEqual(response.data[0]['id'], str(pinnedEntry1.id)) + # self.assertEqual(response.data[0]['pinned'], True) + # # to test that the latest blog appears just below the pinned one def test_blog_order_without_pin(self): """Test ordering of blog with no blog is pinned""" @@ -77,11 +78,11 @@ def test_blog_order_without_pin(self): BlogEntry.objects.all().delete() # creating new posts with no pinned post - first_entry = BlogEntry.objects.create(title="PEntry1", blog_url=settings.PLACEMENTS_URL) - BlogEntry.objects.create(title="PEntry2", blog_url=settings.PLACEMENTS_URL) - BlogEntry.objects.create(title="PEntry3", blog_url=settings.PLACEMENTS_URL) - BlogEntry.objects.create(title="PEntry4", blog_url=settings.PLACEMENTS_URL) - latest_entry = BlogEntry.objects.create(title="PEntry5", blog_url=settings.PLACEMENTS_URL) + first_entry = BlogEntry.objects.create(title="PEntry1", blog_url=settings.PLACEMENTS_URL_VAL) + BlogEntry.objects.create(title="PEntry2", blog_url=settings.PLACEMENTS_URL_VAL) + BlogEntry.objects.create(title="PEntry3", blog_url=settings.PLACEMENTS_URL_VAL) + BlogEntry.objects.create(title="PEntry4", blog_url=settings.PLACEMENTS_URL_VAL) + latest_entry = BlogEntry.objects.create(title="PEntry5", blog_url=settings.PLACEMENTS_URL_VAL) user = get_new_user() self.client.force_authenticate(user) # pylint: disable=E1101 @@ -123,8 +124,11 @@ def test_placements_chore(self): call_command('placement_blog_chore') # Check if posts were collected - placements = lambda: BlogEntry.objects.all().filter(blog_url=settings.PLACEMENTS_URL) - trainings = lambda: BlogEntry.objects.all().filter(blog_url=settings.TRAINING_BLOG_URL) + def placements(): + return BlogEntry.objects.all().filter(blog_url=settings.PLACEMENTS_URL_VAL) + + def trainings(): + return BlogEntry.objects.all().filter(blog_url=settings.TRAINING_BLOG_URL_VAL) self.assertEqual(placements().count(), 3) self.assertEqual(trainings().count(), 5) self.assertEqual(set(x.guid for x in placements()), set('sample:p:%i' % i for i in range(1, 4))) diff --git a/placements/views.py b/placements/views.py index 2ec72755..f9d942ef 100644 --- a/placements/views.py +++ b/placements/views.py @@ -13,9 +13,9 @@ class PlacementBlogViewset(viewsets.ViewSet): @login_required_ajax def placement_blog(cls, request): """Get Placement Blog.""" - queryset = BlogEntry.objects.filter(blog_url=settings.PLACEMENTS_URL) + queryset = BlogEntry.objects.filter(blog_url=settings.PLACEMENTS_URL_VAL) queryset = query_search(request, 3, queryset, ['title', 'content'], 'placement') - queryset = queryset.order_by('-pinned', "-published") + # queryset = queryset.order_by('-pinned', "-published") queryset = query_from_num(request, 20, queryset) return Response(BlogEntrySerializer(queryset, many=True).data) @@ -24,8 +24,8 @@ def placement_blog(cls, request): @login_required_ajax def training_blog(cls, request): """Get Training Blog.""" - queryset = BlogEntry.objects.filter(blog_url=settings.TRAINING_BLOG_URL) + queryset = BlogEntry.objects.filter(blog_url=settings.TRAINING_BLOG_URL_VAL) queryset = query_search(request, 3, queryset, ['title', 'content'], 'training') - queryset = queryset.order_by('-pinned', "-published") + # queryset = queryset.order_by('-pinned', "-published") queryset = query_from_num(request, 20, queryset) return Response(BlogEntrySerializer(queryset, many=True).data) diff --git a/querybot/admin.py b/querybot/admin.py index aa97a232..ef439a53 100644 --- a/querybot/admin.py +++ b/querybot/admin.py @@ -1,8 +1,9 @@ -from notifications.signals import notify - +import csv from django.contrib import admin from django.contrib.auth.models import User -from querybot.models import Query, UnresolvedQuery +from django.http import HttpResponse +from notifications.signals import notify +from querybot.models import Query, UnresolvedQuery, ChatBotLog def handle_entry(entry, notify_user=True): """Handle a single entry from a feed.""" @@ -38,5 +39,21 @@ class UnresolvedQueryAdmin(admin.ModelAdmin): list_filter = ['category', 'resolved'] actions = [make_resolved] +def export_as_csv(self, request, queryset): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename=a.csv' + writer = csv.writer(response) + + writer.writerow(['question', 'answer', 'reaction']) + for obj in queryset: + writer.writerow([obj.question, obj.answer, obj.reaction]) + return response + +class ChatBotLogAdmin(admin.ModelAdmin): + search_fields = ['question', 'answer'] + list_display = ('question', 'answer', 'reaction') + actions = [export_as_csv] + admin.site.register(UnresolvedQuery, UnresolvedQueryAdmin) +admin.site.register(ChatBotLog, ChatBotLogAdmin) diff --git a/querybot/migrations/0003_chatbotlog.py b/querybot/migrations/0003_chatbotlog.py new file mode 100644 index 00000000..58397865 --- /dev/null +++ b/querybot/migrations/0003_chatbotlog.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2022-11-16 18:43 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('querybot', '0002_auto_20211205_1333'), + ] + + operations = [ + migrations.CreateModel( + name='ChatBotLog', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('question', models.TextField()), + ('answer', models.TextField(blank=True)), + ('reaction', models.IntegerField()), + ], + options={ + 'verbose_name': 'UnresolvedQuery', + 'verbose_name_plural': 'UnresolvedQueries', + }, + ), + ] diff --git a/querybot/migrations/0004_alter_chatbotlog_options.py b/querybot/migrations/0004_alter_chatbotlog_options.py new file mode 100644 index 00000000..c9e24231 --- /dev/null +++ b/querybot/migrations/0004_alter_chatbotlog_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-11-16 19:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('querybot', '0003_chatbotlog'), + ] + + operations = [ + migrations.AlterModelOptions( + name='chatbotlog', + options={'verbose_name': 'ChatBotLog', 'verbose_name_plural': 'ChatBotLogs'}, + ), + ] diff --git a/querybot/models.py b/querybot/models.py index 79e800fd..a681bc00 100644 --- a/querybot/models.py +++ b/querybot/models.py @@ -34,3 +34,18 @@ def __str__(self): class Meta: verbose_name = "UnresolvedQuery" verbose_name_plural = "UnresolvedQueries" + +class ChatBotLog(models.Model): + """Reaction to an answer by chatbot""" + + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + question = models.TextField(blank=False) + answer = models.TextField(blank=True) + reaction = models.IntegerField() + + def __str__(self): + return self.question + + class Meta: + verbose_name = "ChatBotLog" + verbose_name_plural = "ChatBotLogs" diff --git a/roles/admin.py b/roles/admin.py index cebe47ae..6feb8101 100644 --- a/roles/admin.py +++ b/roles/admin.py @@ -2,5 +2,15 @@ from roles.models import BodyRole from roles.models import InstituteRole -admin.site.register(BodyRole) -admin.site.register(InstituteRole) +class BodyRoleAdmin(admin.ModelAdmin): + list_filter = ['body'] + list_display = ('name', 'body', 'permissions') + search_fields = ('body__name', 'name') + +class InstittuteRoleAdmin(admin.ModelAdmin): + list_display = ('name', 'permissions') + search_fields = ['name'] + + +admin.site.register(BodyRole, BodyRoleAdmin) +admin.site.register(InstituteRole, InstittuteRoleAdmin) diff --git a/roles/helpers.py b/roles/helpers.py index 8a9a733c..725faa27 100644 --- a/roles/helpers.py +++ b/roles/helpers.py @@ -1,6 +1,7 @@ """Helper functions for implementing roles.""" from rest_framework.response import Response from bodies.models import Body +from community.models import Community def forbidden_no_privileges(): """Forbidden due to insufficient privileges.""" @@ -17,6 +18,14 @@ def user_has_privilege(profile, bodyid, privilege): if (role.body == body or role.inheritable) and privilege in role.permissions: return True return False +def user_has_community_privilege(profile, communityid, privilege): + """Returns true if UserProfile has or has inherited the privilege.""" + community = Community.objects.get(pk=communityid) + parents = get_parents_recursive(community, []) + for role in profile.roles.all().filter(community__in=parents): + if (role.body == community or role.inheritable) and privilege in role.permissions: + return True + return False def user_has_insti_privilege(profile, privilege): """Returns true if UserProfile has the institute privilege.""" diff --git a/roles/migrations/0012_alter_bodyrole_permissions.py b/roles/migrations/0012_alter_bodyrole_permissions.py new file mode 100644 index 00000000..809d89a1 --- /dev/null +++ b/roles/migrations/0012_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-07 17:44 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0011_auto_20190719_2321'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('ModP', 'Moderate Post'), ('ModC', 'Moderate Comment')], max_length=39), + ), + ] diff --git a/roles/migrations/0013_alter_bodyrole_permissions.py b/roles/migrations/0013_alter_bodyrole_permissions.py new file mode 100644 index 00000000..7a561505 --- /dev/null +++ b/roles/migrations/0013_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-08 12:56 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0012_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AddP', 'Add Post'), ('AddC', 'Add Comment'), ('UpdP', 'Update Post'), ('UpdC', 'Update Comment'), ('ModP', 'Moderate Post'), ('ModC', 'Moderate Comment'), ('DelP', 'Delete Post'), ('DelC', 'Delete Comment')], max_length=69), + ), + ] diff --git a/roles/migrations/0014_alter_bodyrole_permissions.py b/roles/migrations/0014_alter_bodyrole_permissions.py new file mode 100644 index 00000000..0cc74128 --- /dev/null +++ b/roles/migrations/0014_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-14 08:21 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0013_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AppP', 'Approve Post'), ('DelP', 'Delete Post')], max_length=39), + ), + ] diff --git a/roles/migrations/0015_alter_bodyrole_permissions.py b/roles/migrations/0015_alter_bodyrole_permissions.py new file mode 100644 index 00000000..db8baf34 --- /dev/null +++ b/roles/migrations/0015_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-15 13:35 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0014_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AppP', 'Approve Post'), ('DelP', 'Delete Post'), ('FeaP', 'Feature Post')], max_length=44), + ), + ] diff --git a/roles/migrations/0016_alter_bodyrole_permissions.py b/roles/migrations/0016_alter_bodyrole_permissions.py new file mode 100644 index 00000000..df9f07c5 --- /dev/null +++ b/roles/migrations/0016_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-16 08:53 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0015_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AppP', 'Approve Post'), ('DelP', 'Delete Post'), ('FeaP', 'Feature Post'), ('ModC', 'Moderate Comment')], max_length=49), + ), + ] diff --git a/roles/migrations/0017_alter_bodyrole_permissions.py b/roles/migrations/0017_alter_bodyrole_permissions.py new file mode 100644 index 00000000..7ec53348 --- /dev/null +++ b/roles/migrations/0017_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-08-22 18:42 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0016_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AppP', 'Approve Post'), ('DelP', 'Delete Post'), ('FeaP', 'Feature Post'), ('ModC', 'Moderate Comment'), ('RepC', 'Report Comment')], max_length=54), + ), + ] diff --git a/roles/migrations/0018_alter_bodyrole_permissions.py b/roles/migrations/0018_alter_bodyrole_permissions.py new file mode 100644 index 00000000..e728e57d --- /dev/null +++ b/roles/migrations/0018_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-08-27 17:33 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0017_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AppP', 'Approve Post'), ('ModC', 'Moderate Comment')], max_length=39), + ), + ] diff --git a/roles/migrations/0019_alter_bodyrole_permissions.py b/roles/migrations/0019_alter_bodyrole_permissions.py new file mode 100644 index 00000000..270b3826 --- /dev/null +++ b/roles/migrations/0019_alter_bodyrole_permissions.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-10-03 11:35 + +from django.db import migrations +import multiselectfield.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('roles', '0018_alter_bodyrole_permissions'), + ] + + operations = [ + migrations.AlterField( + model_name='bodyrole', + name='permissions', + field=multiselectfield.db.fields.MultiSelectField(choices=[('AddE', 'Add Event'), ('UpdE', 'Update Event'), ('DelE', 'Delete Event'), ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), ('AppP', 'Moderate Post'), ('ModC', 'Moderate Comment')], max_length=39), + ), + ] diff --git a/roles/models.py b/roles/models.py index 6380716b..a1267b24 100644 --- a/roles/models.py +++ b/roles/models.py @@ -10,6 +10,8 @@ ('UpdB', 'Update Body'), ('Role', 'Modify Roles'), ('VerA', 'Verify Achievements'), + ('AppP', 'Moderate Post'), + ('ModC', 'Moderate Comment') ) class BodyRole(models.Model): diff --git a/upload/management/commands/clean-images.py b/upload/management/commands/clean-images.py index d33b3c56..4bbb7c3c 100644 --- a/upload/management/commands/clean-images.py +++ b/upload/management/commands/clean-images.py @@ -1,9 +1,11 @@ from django.core.management.base import BaseCommand from django.utils import timezone +from django.db.models import Q from upload.models import UploadedImage from events.models import Event from bodies.models import Body from venter.models import ComplaintImage +from community.models import Community, CommunityPost class Command(BaseCommand): help = 'Check claims and clean unclaimed images.' @@ -26,7 +28,9 @@ def handle(self, *args, **options): queries = [ Event.objects.filter(image_url__contains=url), Body.objects.filter(image_url__contains=url), - ComplaintImage.objects.filter(image_url__contains=url) + ComplaintImage.objects.filter(image_url__contains=url), + Community.objects.filter(Q(logo_image__contains=url) | Q(cover_image__contains=url)), + CommunityPost.objects.filter(image_url__contains=url) ] # Look for claimants @@ -49,7 +53,12 @@ def handle(self, *args, **options): # Validate claims on claimed images verified = 0 for image in UploadedImage.objects.prefetch_related('claimant').filter(is_claimed=True): - if not image.claimant or image.picture.url not in image.claimant.image_url: + if (not image.claimant # pylint: disable=R0916 + or (not isinstance(image.claimant, Community) + and image.picture.url not in image.claimant.image_url) + or (isinstance(image.claimant, Community) + and image.picture.url not in image.claimant.logo_image + and image.picture.url not in image.claimant.cover_image)): print('Invalid claimant for', image.picture.url, '(%s)' % str(image.uploaded_by)) image.delete() cleaned += 1 diff --git a/upload/tests.py b/upload/tests.py index 382c2eac..c2c9951f 100644 --- a/upload/tests.py +++ b/upload/tests.py @@ -14,10 +14,12 @@ from upload.models import UploadedImage -get_image = lambda: SimpleUploadedFile( - "img.jpg", open("./upload/img.jpg", "rb").read(), content_type="image/jpeg") +def get_image(): + return SimpleUploadedFile("img.jpg", open("./upload/img.jpg", "rb").read(), content_type="image/jpeg") -new_upload = lambda slf: slf.client.post('/api/upload', {'picture': get_image()}) + +def new_upload(slf): + return slf.client.post('/api/upload', {'picture': get_image()}) class UploadTestCase(APITestCase): """Check if logged in users can upload files.""" @@ -67,8 +69,11 @@ def test_clean_images_chore(self): body1 = create_body(image_url=res[3].data['picture']) # Get path for checking deletion - obj = lambda res: UploadedImage.objects.get(pk=res.data['id']) - obj_exists = lambda res: UploadedImage.objects.filter(pk=res.data['id']).exists() + def obj(res): + return UploadedImage.objects.get(pk=res.data['id']) + + def obj_exists(res): + return UploadedImage.objects.filter(pk=res.data['id']).exists() paths = [obj(r).picture.path for r in res] # Check if deleting a non existent file is fine @@ -79,7 +84,8 @@ def test_clean_images_chore(self): self.assertFalse(obj_exists(res[4])) # Call the chore - clean = lambda: call_command('clean-images') + def clean(): + return call_command('clean-images') clean() # Check if unclaimed images were removed diff --git a/users/admin.py b/users/admin.py index fbb75974..fbeafa39 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,15 +1,28 @@ +import csv from django.contrib import admin +from django.http import HttpResponse from users.models import UserProfile from users.models import UserFormerRole from users.models import WebPushSubscription from users.models import UserTagCategory from users.models import UserTag +def export_as_csv(self, request, queryset): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename=a.csv' + writer = csv.writer(response) + + writer.writerow(['name', 'roll_no', 'contact']) + for obj in queryset: + writer.writerow([obj.name, obj.roll_no, obj.contact_no]) + return response + class ProfileAdmin(admin.ModelAdmin): search_fields = ['name', 'roll_no', 'ldap_id'] list_display = ('name', 'roll_no', 'department', 'degree', 'last_ping') list_filter = ('last_ping', 'join_year', 'department', 'degree') raw_id_fields = ('user',) + actions = [export_as_csv] class UserFormerRoleAdmin(admin.ModelAdmin): list_display = ('user', 'role', 'year') diff --git a/users/migrations/0039_userprofile_followed_communities.py b/users/migrations/0039_userprofile_followed_communities.py new file mode 100644 index 00000000..d5795114 --- /dev/null +++ b/users/migrations/0039_userprofile_followed_communities.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-07-07 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0009_community_followers'), + ('users', '0038_auto_20210606_2237'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='followed_communities', + field=models.ManyToManyField(blank=True, related_name='followers_Community', to='community.Community'), + ), + ] diff --git a/users/migrations/0040_remove_userprofile_followed_communities.py b/users/migrations/0040_remove_userprofile_followed_communities.py new file mode 100644 index 00000000..b3c9518e --- /dev/null +++ b/users/migrations/0040_remove_userprofile_followed_communities.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.10 on 2022-08-14 14:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0039_userprofile_followed_communities'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='followed_communities', + ), + ] diff --git a/users/models.py b/users/models.py index 2f5089d9..4e4b3ad5 100644 --- a/users/models.py +++ b/users/models.py @@ -48,7 +48,6 @@ class UserProfile(models.Model): # InstiApp feature fields active = models.BooleanField(default=True) followed_bodies = models.ManyToManyField('bodies.Body', related_name='followers', blank=True) - # InstiApp roles roles = models.ManyToManyField('roles.BodyRole', related_name='users', blank=True) former_roles = models.ManyToManyField( @@ -75,7 +74,7 @@ class ExMeta: user_editable = ('show_contact_no', 'fcm_id', 'about', 'android_version', 'website_url') def __str__(self): - return self.name + return f"{self.name} - {self.roll_no}" class UserFormerRole(models.Model): """Through field for former_role from UserProfile to BodyRole.""" diff --git a/users/tests.py b/users/tests.py index 781338ce..780db9f2 100644 --- a/users/tests.py +++ b/users/tests.py @@ -27,10 +27,10 @@ def test_get_user(self): contact = '9876543210' profile = UserProfile.objects.create( - name="TestUser", ldap_id="tu", contact_no=contact) + name="TestUser", roll_no="tu", ldap_id="tu", contact_no=contact) # Check __str__ - self.assertEqual(str(profile), profile.name) + self.assertEqual(str(profile), profile.name + ' - ' + profile.roll_no) # Test GET with UUID url = '/api/users/' + str(profile.id) @@ -56,7 +56,8 @@ def test_user_me(self): """Check the /api/user-me API.""" # Function to get latest user from database - usr = lambda: UserProfile.objects.get(id=self.user.profile.id) + def usr(): + return UserProfile.objects.get(id=self.user.profile.id) # Initialize self.user.profile.fcm_id = 'TESTINIT' @@ -163,7 +164,7 @@ def test_user_me(self): dev.application = 'app.insti.flutter' self.assertEqual(dev.supports_rich(), False) self.assertNotEqual(dev.process_rich(data)['click_action'], None) - dev.application = 'app.insti.ios' + dev.application = 'app.instiapp.flutter' self.assertEqual(dev.supports_rich(), False) def test_get_noauth(self): diff --git a/users/urls.py b/users/urls.py index 40aabf95..85a85a3e 100644 --- a/users/urls.py +++ b/users/urls.py @@ -14,6 +14,8 @@ path('user-me/ues/', UserProfileViewSet.as_view({'get': 'set_ues_me'})), path('user-me/unr/', UserProfileViewSet.as_view({'get': 'set_unr_me'})), + path('user-me/ucr/', UserProfileViewSet.as_view({'post': 'set_ucr_me'})), + path('user-me/ucpr/', UserProfileViewSet.as_view({'get': 'set_upr_me'})), path('user-me/subscribe-wp', UserProfileViewSet.as_view({'post': 'subscribe_web_push'})), path('user-me/events', UserProfileViewSet.as_view({'get': 'get_my_events'})), path('user-me/roles', BodyRoleViewSet.as_view({'get': 'get_my_roles'})), diff --git a/users/views.py b/users/views.py index 57208e75..8caed22f 100644 --- a/users/views.py +++ b/users/views.py @@ -4,7 +4,6 @@ from rest_framework.response import Response from django.shortcuts import get_object_or_404 from django.utils import timezone - from login.helpers import update_fcm_device from events.models import UserEventStatus @@ -12,11 +11,13 @@ from events.serializers import EventSerializer from news.models import UserNewsReaction from news.models import NewsEntry +from community.models import CommunityPost, CommunityPostUserReaction from users.serializer_full import UserProfileFullSerializer from users.models import UserProfile from users.models import WebPushSubscription from roles.helpers import login_required_ajax from roles.helpers import forbidden_no_privileges +from querybot.models import ChatBotLog class UserProfileViewSet(viewsets.ModelViewSet): """UserProfile""" @@ -132,6 +133,49 @@ def set_unr_me(cls, request, news_pk): unr.save() return Response(status=204) + @classmethod + @login_required_ajax + def set_ucr_me(cls, request): + """Set UNR(User News Reaction) for current user. + This will create or update if record exists.""" + + # Get reaction from query parameter + reaction = request.data['reaction'] + answer = request.data['answer'] + question = request.data['question'] + if reaction is None or answer is None or question is None: + return Response({"message": "reaction is required"}, status=400) + + ChatBotLog.objects.create(question=question, answer=answer, reaction=reaction) + + return Response(status=204) + + @classmethod + @login_required_ajax + def set_upr_me(cls, request, post_pk): + """Set UPR(User Post Reaction) for current user. + This will create or update if record exists.""" + + # Get reaction from query parameter + reaction = request.GET.get('reaction') + if reaction is None: + return Response({"message": "reaction is required"}, status=400) + + # Get existing record if it exists + upr = CommunityPostUserReaction.objects.filter(communitypost__id=post_pk, user=request.user.profile).first() + + # Create new UserNewsReaction if not existing + if not upr: + get_post = get_object_or_404(CommunityPost.objects.all(), pk=post_pk) + CommunityPostUserReaction.objects.create( + communitypost=get_post, user=request.user.profile, reaction=reaction) + return Response(status=204) + + # Update existing UserNewsReaction + upr.reaction = reaction + upr.save() + return Response(status=204) + @classmethod @login_required_ajax def get_my_events(cls, request): diff --git a/venter/views.py b/venter/views.py index 1d4f6161..e42a5f87 100644 --- a/venter/views.py +++ b/venter/views.py @@ -192,7 +192,7 @@ class CommentViewSet(viewsets.ModelViewSet): @classmethod @login_required_ajax - def create(cls, request, pk): + def create(cls, request, pk): # pylint: disable=W0237 get_complaint = get_object_or_404(Complaint.objects.all(), id=pk) get_text = request.data['text'] comment = ComplaintComment.objects.create(