From 16de163dd47d3bfdb1b1889be65f2552fcaf48d8 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 10:20:40 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20First=20implementation=20of=20p?= =?UTF-8?q?ersonal=20archives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/accounts/users.py | 19 --------- schoolsyst_api/personal_archive/__init__.py | 0 schoolsyst_api/personal_archive/models.py | 23 +++++++++++ schoolsyst_api/personal_archive/routes.py | 46 +++++++++++++++++++++ schoolsyst_api/utils.py | 20 +++++++++ 5 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 schoolsyst_api/personal_archive/__init__.py create mode 100644 schoolsyst_api/personal_archive/models.py create mode 100644 schoolsyst_api/personal_archive/routes.py diff --git a/schoolsyst_api/accounts/users.py b/schoolsyst_api/accounts/users.py index 19c2ba9..5ec116c 100644 --- a/schoolsyst_api/accounts/users.py +++ b/schoolsyst_api/accounts/users.py @@ -210,22 +210,3 @@ async def delete_current_user( db.collection(c).delete_match({"owner_key": user.key}) return Response(status_code=status.HTTP_204_NO_CONTENT) - - -@router.get("/personal_data_archive") -async def get_personal_data_archive( - user: User = Depends(get_current_confirmed_user), - db: StandardDatabase = Depends(database.get), -) -> dict: - """ - Get an archive of all of the data linked to the user. - """ - data = {} - # The user's data - data["user"] = db.collection("users").get(user.key) - # the data of which the user is the owner for every collection - for c in COLLECTIONS: - if c == "users": - continue - data[c] = [batch for batch in db.collection(c).find({"owner_key": user.key})] - return data diff --git a/schoolsyst_api/personal_archive/__init__.py b/schoolsyst_api/personal_archive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schoolsyst_api/personal_archive/models.py b/schoolsyst_api/personal_archive/models.py new file mode 100644 index 0000000..4d7c637 --- /dev/null +++ b/schoolsyst_api/personal_archive/models.py @@ -0,0 +1,23 @@ +from typing import List + +from schoolsyst_api.accounts.models import User +from schoolsyst_api.grades.models import Grade +from schoolsyst_api.homework.models import Homework +from schoolsyst_api.learn.models import Quiz +from schoolsyst_api.models import BaseModel +from schoolsyst_api.notes.models import Note +from schoolsyst_api.schedule.models import Event, EventMutation +from schoolsyst_api.settings.models import Settings +from schoolsyst_api.subjects.models import Subject + + +class PersonalArchive(BaseModel): + subjects: List[Subject] + users: List[User] + settings: List[Settings] + quizzes: List[Quiz] + notes: List[Note] + grades: List[Grade] + homework: List[Homework] + events: List[Event] + event_mutations: List[EventMutation] diff --git a/schoolsyst_api/personal_archive/routes.py b/schoolsyst_api/personal_archive/routes.py new file mode 100644 index 0000000..8a93136 --- /dev/null +++ b/schoolsyst_api/personal_archive/routes.py @@ -0,0 +1,46 @@ +from arango.database import StandardDatabase +from fastapi import Depends, Response, status +from fastapi_utils.inferring_router import InferringRouter +from schoolsyst_api import database +from schoolsyst_api.accounts.models import User +from schoolsyst_api.accounts.users import get_current_confirmed_user +from schoolsyst_api.database import COLLECTIONS +from schoolsyst_api.personal_archive.models import PersonalArchive +from schoolsyst_api.utils import zip_text + +router = InferringRouter() + + +@router.get( + "/personal_data_archive", + description=f"""\ +Get an archive of all the data owned by the user. +The response is a zip file containing a JSON response, which is +an object associating keys {', '.join(COLLECTIONS)} +to lists of the corresponding objects.""", +) +async def get_personal_data_archive( + filename: str = "schoolsyst_data_archive.zip", + user: User = Depends(get_current_confirmed_user), + db: StandardDatabase = Depends(database.get), +): + """ + Get an archive of all of the data linked to the user. + """ + data = {} + # The user's data + data["user"] = db.collection("users").get(user.key) + # the data of which the user is the owner for every collection + for c in COLLECTIONS: + if c == "users": + continue + data[c] = [batch for batch in db.collection(c).find({"owner_key": user.key})] + + return Response( + content=zip_text( + PersonalArchive(**data).json(by_alias=True), filename="archive.json" + ), + status_code=status.HTTP_201_CREATED, + headers={"Content-Disposition": f"attachment; filename={filename}"}, + media_type="application/x-zip-compressed", + ) diff --git a/schoolsyst_api/utils.py b/schoolsyst_api/utils.py index a3eb2d2..e99029d 100644 --- a/schoolsyst_api/utils.py +++ b/schoolsyst_api/utils.py @@ -1,6 +1,9 @@ from datetime import date, datetime, timedelta from decimal import Decimal +from io import BytesIO +from tempfile import TemporaryFile from typing import Iterator, TypeVar +from zipfile import ZipFile from isodate import duration_isoformat @@ -45,3 +48,20 @@ def daterange(start: D, end: D, precision: str = "days") -> Iterator[D]: if precision == "weeks": n *= 7 yield start + timedelta(**{precision: n}) + + +def zip_text(text: str, filename: str) -> bytes: + # Open StringIO to grab in-memory ZIP contents + buffer = BytesIO() + file = ZipFile(buffer, "w") + + with TemporaryFile("w") as temp_file: + # Write the contents from text into a temprary file + temp_file.write(text) + # Write that zip file into the archive + file.write(temp_file.name, filename) + + # Close the archive + file.close() + + return buffer.getvalue() From b193d44230ee552b3a7face8d695dba593115820 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 10:38:15 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9E=95=20Depend=20on=20pyzip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 6052fac..34e1b4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -787,6 +787,14 @@ optional = false python-versions = "*" version = "5.3.1" +[[package]] +category = "main" +description = "PyZip is a package for handling zip-in-memory content as a dictionary." +name = "pyzip" +optional = false +python-versions = "*" +version = "0.2.0" + [[package]] category = "dev" description = "Alternative regular expression module, to replace re." @@ -1028,7 +1036,7 @@ python-versions = "*" version = "4.4.28" [metadata] -content-hash = "1f318bc4c8d62b545037d45083cc00373624eed3ded4f31ed9baf6b8570c9a4c" +content-hash = "79db42891299f61d4e4c8bb21f59f1ca196eee0eb5bb268779674c77d1b22ba5" lock-version = "1.1" python-versions = "^3.8" @@ -1508,6 +1516,9 @@ pyyaml = [ {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] +pyzip = [ + {file = "pyzip-0.2.0.tar.gz", hash = "sha256:c0b10776d798e4be9d5fe6eec541dd0a9740b6550b12fd4cfa238a085686a298"}, +] regex = [ {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, diff --git a/pyproject.toml b/pyproject.toml index fdcfe81..bbb72d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ nanoid = "^2.0.0" python-slugify = "^4.0.1" fastapi_utils = "^0.2.1" fastapi-etag = "^0.2.2" +pyzip = "^0.2.0" [tool.poetry.dev-dependencies] black = "^19.10b0" From a8991562d6b084720273cf4178d6a6d68f2e9c53 Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 10:38:48 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9B=20Include=20personal=5Farchive?= =?UTF-8?q?'s=20router?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/schoolsyst_api/main.py b/schoolsyst_api/main.py index 9ec18d5..8b253fd 100644 --- a/schoolsyst_api/main.py +++ b/schoolsyst_api/main.py @@ -1,6 +1,7 @@ from pathlib import Path import schoolsyst_api.homework.routes +import schoolsyst_api.personal_archive.routes import schoolsyst_api.schedule.routes import schoolsyst_api.settings.routes import schoolsyst_api.subjects.routes @@ -29,6 +30,7 @@ api.add_middleware(**cors.middleware_params) # Include routes api.include_router(accounts.router, tags=["Accounts"]) +api.include_router(schoolsyst_api.personal_archive.routes.router, tags=["Accounts"]) api.include_router(schoolsyst_api.subjects.routes.router, tags=["Subjects"]) api.include_router(schoolsyst_api.homework.routes.router, tags=["Homework"]) api.include_router(schoolsyst_api.settings.routes.router, tags=["Settings"]) From 811d9fa30613a6aee8ab8ca28d93b71a9d78d15c Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 10:39:34 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20PyZip=20for=20pe?= =?UTF-8?q?rsonal=20archive=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/personal_archive/routes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/schoolsyst_api/personal_archive/routes.py b/schoolsyst_api/personal_archive/routes.py index 8a93136..f43de06 100644 --- a/schoolsyst_api/personal_archive/routes.py +++ b/schoolsyst_api/personal_archive/routes.py @@ -1,12 +1,12 @@ from arango.database import StandardDatabase from fastapi import Depends, Response, status from fastapi_utils.inferring_router import InferringRouter +from pyzip import PyZip from schoolsyst_api import database from schoolsyst_api.accounts.models import User from schoolsyst_api.accounts.users import get_current_confirmed_user from schoolsyst_api.database import COLLECTIONS from schoolsyst_api.personal_archive.models import PersonalArchive -from schoolsyst_api.utils import zip_text router = InferringRouter() @@ -29,18 +29,18 @@ async def get_personal_data_archive( """ data = {} # The user's data - data["user"] = db.collection("users").get(user.key) + data["users"] = [db.collection("users").get(user.key)] # the data of which the user is the owner for every collection for c in COLLECTIONS: if c == "users": continue data[c] = [batch for batch in db.collection(c).find({"owner_key": user.key})] - + # zip the data + zip_file = PyZip() + zip_file["data.json"] = PersonalArchive(**data).json(by_alias=True).encode("utf-8") return Response( - content=zip_text( - PersonalArchive(**data).json(by_alias=True), filename="archive.json" - ), + content=zip_file.to_bytes(), status_code=status.HTTP_201_CREATED, headers={"Content-Disposition": f"attachment; filename={filename}"}, - media_type="application/x-zip-compressed", + media_type="application/zip", ) From 41ed2d85366259468eeb904ab2d57c3841941cdc Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 10:39:53 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=94=A5=20Remove=20useless=20`utils.zi?= =?UTF-8?q?p=5Ftext`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- schoolsyst_api/utils.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/schoolsyst_api/utils.py b/schoolsyst_api/utils.py index e99029d..a3eb2d2 100644 --- a/schoolsyst_api/utils.py +++ b/schoolsyst_api/utils.py @@ -1,9 +1,6 @@ from datetime import date, datetime, timedelta from decimal import Decimal -from io import BytesIO -from tempfile import TemporaryFile from typing import Iterator, TypeVar -from zipfile import ZipFile from isodate import duration_isoformat @@ -48,20 +45,3 @@ def daterange(start: D, end: D, precision: str = "days") -> Iterator[D]: if precision == "weeks": n *= 7 yield start + timedelta(**{precision: n}) - - -def zip_text(text: str, filename: str) -> bytes: - # Open StringIO to grab in-memory ZIP contents - buffer = BytesIO() - file = ZipFile(buffer, "w") - - with TemporaryFile("w") as temp_file: - # Write the contents from text into a temprary file - temp_file.write(text) - # Write that zip file into the archive - file.write(temp_file.name, filename) - - # Close the archive - file.close() - - return buffer.getvalue() From 0a24ac32e1ca0656f2b4112ee98d2123f5dd226c Mon Sep 17 00:00:00 2001 From: Ewen Le Bihan Date: Mon, 31 Aug 2020 10:42:16 +0200 Subject: [PATCH 6/6] Implement personal data archive (Closes #22) --- schoolsyst_api/personal_archive/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/schoolsyst_api/personal_archive/routes.py b/schoolsyst_api/personal_archive/routes.py index f43de06..c84a107 100644 --- a/schoolsyst_api/personal_archive/routes.py +++ b/schoolsyst_api/personal_archive/routes.py @@ -13,6 +13,7 @@ @router.get( "/personal_data_archive", + status_code=status.HTTP_201_CREATED, description=f"""\ Get an archive of all the data owned by the user. The response is a zip file containing a JSON response, which is