diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 0000000..f9f1e2b --- /dev/null +++ b/.cruft.json @@ -0,0 +1,23 @@ +{ + "template": "git@github.com:vorausrobotik/voraus-python-template.git", + "commit": "9ef5161b20a68d274ecf3828467c3f44f4747bca", + "checkout": null, + "context": { + "cookiecutter": { + "full_name": "Jan-Frederik Schmidt", + "email": "janfschmidt@mailbox.org", + "github_user": "", + "project_name": "shop-db2", + "repo_name": "shop-db2", + "package_name": "shop-db2", + "import_name": "shop_db2", + "project_short_description": "The simple way to manage purchases and user interactions in a small community.", + "url": "https://github.com/g3n35i5/shop-db2", + "_debug": "False", + "_extensions": ["extensions.git_extension.GitExtension"], + "_copy_without_render": ["docs/_templates/license_compliance.rst.j2"], + "_template": "git@github.com:vorausrobotik/voraus-python-template.git" + } + }, + "directory": null +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1694d6d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @g3n35i5 \ No newline at end of file diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..d25457e --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,100 @@ +name: CI pipeline + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + python-version: ["3.8", "3.9"] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install wkhtmltopdf + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + sudo apt-get install xvfb libfontconfig wkhtmltopdf + elif [ "$RUNNER_OS" == "macOS" ]; then + brew install wkhtmltopdf + fi + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: python -m pip install --upgrade pip tox tox-gh-actions + + - name: Write example configuration file + run: cp configuration.example.py configuration.py + + - name: Run tests + run: tox + + - name: Upload coverage artifact + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: pytest-${{ matrix.python-version }} + path: reports/* + + coverage: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: test + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all coverage artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: reports + + - uses: actions/setup-python@v5 + with: + python-version: 3.9 + + - name: Install tox + run: python -m pip install --upgrade pip tox + + - name: Combine coverage results + run: tox run -e combine-test-reports + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: reports/coverage.xml + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: 3.8 + + - name: Install tox + run: python -m pip install --upgrade tox + + - name: Run static checks + run: tox -e lint diff --git a/.github/workflows/pr_lint.yml b/.github/workflows/pr_lint.yml new file mode 100644 index 0000000..8ef41e4 --- /dev/null +++ b/.github/workflows/pr_lint.yml @@ -0,0 +1,47 @@ +name: Pull Request Lint + +on: + pull_request: + types: [opened, edited, synchronize, labeled, unlabeled] + +permissions: + pull-requests: read + +jobs: + main: + name: Validate PR title and description + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + requireScope: false + # Configure additional validation for the subject based on a regex. + # This ensures the subject does start with an uppercase character. + subjectPattern: ^(?![a-z])(?![a-zA-Z]+s\s(?= 3.7) -- Python 3 Virtual Environment -- pip3 -- git -- nginx - -```bash -sudo apt install python3 python3-venv python3-pip git nginx -``` - -### Optional -- wkhtmltopdf (For generating pdf templates) - -```bash -sudo apt install wkhtmltopdf -``` - -## Getting started - -Add an account for shop-db called shopdb_user. Since this account is only for -running shop-db the extra arguments of -r is added to create a system -account without creating a home directory: - -```bash -sudo useradd -r shopdb_user -``` - -Next we will create a directory for the installation of shop-db and change -the owner to the shopdb_user account: - -```bash -cd /srv -sudo git clone https://github.com/g3n35i5/shop-db2 -sudo chown -R shopdb_user:shopdb_user shop-db2 -``` - -Now the Nginx server must be configured. Nginx installs a test site in this -location that we wont't need, so let's remove it: - -```bash -sudo rm /etc/nginx/sites-enabled/default -``` - -Below you can see the Nginx configuration file for shop-db, which goes in -/etc/nginx/sites-enabled/shop-db: - -```nginx -server { - # listen on port 80 (http) - listen 80; - server_name shopdb; - location / { - # redirect any requests to the same URL but on https - return 301 https://$host$request_uri; - } -} -server { - # listen on port 443 (https) - listen 443 ssl; - server_name shopdb; - - # New: gzip compression - gzip on; - gzip_static on; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; - gzip_proxied any; - gzip_vary on; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - - # location of the SSL certificates - ssl_certificate /cert.pem; - ssl_certificate_key /key.pem; - - # write access and error logs to /var/log - access_log /var/log/shop-db_access.log; - error_log /var/log/shop-db_error.log; - - location / { - # forward application requests to the gunicorn server - proxy_pass http://localhost:; - proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} -``` - -Next up is to create and change to a virtual environment for shop-db. This -will be done as the shopdb_user account: - -```bash -sudo su -s /bin/bash shopdb_user -cd /srv/shop-db2 -python3 -m venv venv -source venv/bin/activate -# Use source venv/bin/activate.fish if you are using fish-shell -``` - -Once you have activated the virtual environment you will notice the prompt -change and then you can install the required python modules: - -```bash -pip install -r requirements.txt -``` - -Now the configuration file of shop-db2 has to be adjusted. -Copy the `configuration.example.py` to the `configuration.py` file: - -```bash -cp configuration.example.py configuration.py -``` - -The most important change is the SECRET_KEY. This is later responsible for -salting the user passwords and must be kept secret under all circumstances. -Change this SECRET_KEY in the file `configuration.py`. You can do this with a -normal text editor or with the command `sed`: - -```bash -sed -i 's/YouWillNeverGuess/YOURBETTERSUPERSECRETKEY/g' configuration.py -``` - -The first user (and at the same time the first administrator) as well as the -default ranks are created using the `setupdb.py` script. Please look at the -file and check whether the default settings for the ranks meet your -requirements. - -If you are satisfied with them, you can now initialize the database: - -```bash -python ./setupdb.py -``` - -Ready? Almost. To start shop-db, you only have to type: - -```bash -python ./wsgi.py -``` - -However, so that the backend does not have to be started manually every time, it -is advisable to run shop-db as a systemd service: - -```bash -deactivate # To deactivate the virtual environment -exit # To switch back to the root user -sudo nano /etc/systemd/system/shop-db2@shopdb_user.service -``` - -The file must have the following content: - -```ini -[Unit] -Description=shop-db2 -After=network-online.target - -[Service] -Type=simple -User=%i -ExecStart=/srv/shop-db2/venv/bin/python3 /srv/shop-db2/wsgi.py - -[Install] -WantedBy=multi-user.target -``` - -You need to reload systemd to make the daemon aware of the new configuration: - -```bash -sudo systemctl --system daemon-reload -``` - -To have shop-db start automatically at boot, enable the service: - -```bash -sudo systemctl enable shop-db2@shopdb_user -``` - -To disable the automatic start, use this command: - -```bash -sudo systemctl disable shop-db2@shopdb_user -``` - -To start shop-db now, use this command: - -```bash -sudo systemctl start shop-db2@shopdb_user -``` - -## Development - -You want to start shop-db in developer mode and participate in the project? -Great! The command - -```bash -python ./shopdb.py --mode development -``` - -starts shop-db for you in developer mode. This means that a temporary database -is created in memory with default data defined in the dev folder. Your -production database will not be used in this mode, but you should make sure -you have a backup in case something goes wrong. - -## Backups - -To create backups from the database, you can use the `backup.py` script in the -root directory of shop-db. To do this regularly, either a service or a -crobjob can be used. - -### Option 1: systemd service - -Create two files with the following content: - -`/etc/systemd/system/shop-db-backup.service`: -```ini -[Unit] -Description=shop-db2 backup service - -[Service] -Type=oneshot -ExecStart=/srv/shop-db2/venv/bin/python /srv/shop-db2/backup.py -``` - -`/etc/systemd/system/shop-db-backup.timer`: -```ini -[Unit] -Description=Timer for the shop-db2 backup service. - -[Timer] -OnCalendar=00/3 - -[Install] -WantedBy=timers.target -``` - -Reload the services and start the backup service by typing - -```bash -systemctl daemon-reload -systemctl start shop-db2-backup.timer -``` - -If you want to check your timer and the states of the backups, you can use - -```bash -systemctl list-timers --all -``` - -### Option 2: cronjob - -Create the following cronob: - -```bash -0 */3 * * * /srv/shop-db2/backup.py -``` - -## Unittests - -Currently, most of the core features of shop-db are covered with the -corresponding unittests. In order to execute them you can use the command - -```bash -cd /srv/shop-db2 -source venv/bin/activate -./run_tests_with_coverage.py --show-results -``` - -## Models - -This section covers the models used in shop-db. They are defined in -.shopdb/models.py - -### User - -In order to interact with the database one needs some sort of user account -which stores personal data, privileges and that can be referenced by other -parts of the application. Therefor we use the User table. Anyone who can reach -the shop-db application can create a user. After creating User through -registering, one has to be verified by an admin to be able to use ones account. -This prevents unauthorized use of the application. In addition a user can be an -admin, has a rank, a credit to buy products and a list of favorite products. - -| NAME | TYPE | Explanation | -| --------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The user id is unique and is used to identify the user in the application. It is created automatically with the a new user. | -| creation_date | _DateTime_ | This is the date and time the user was created. It is created automatically with the new user. It is not modified when user properties are updated. | -| firstname | _String(32)_ | This is the users firstname. It is used to identify the user in the frontend. It does not have to be unique. It can be updated and changed after the users creation. | -| lastname | _String(32)_ | This is the users lastname. It is used to identify the user in the frontend. It does not have to be unique. It can be updated and changed after the users creation. | -| password | _String(256)_ | This is the password hash which is used to verify the users password when he logs in. It is automatically created from the password passed when creating or updating the user. The password itself is not stored in the database. | -| is_verified | _Boolean_ | To prevent unauthorized access, each user has to be verified from an admin before he can carry out further actions. This column states whether the user is verified (True) or not (False). | -| image_upload_id | _Integer_ | This is the id of the Upload with the user picture. This entry is optional. | - -### UserVerification - -When a user is verified, a UserVerification entry is made. It is used to -separate information about the verification from the user. As a result a user -cant be verified twice and the verification date of a user can be called. A -UserVerification can only be made by an admin. - -| NAME | TYPE | Explanation | -| --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The UserVerification id is unique and is used for identification in the application. It is created automatically with a new UserVerification. | -| timestamp | _DateTime_ | This is the date and time the UserVerification was created. It is created automatically with the new UserVerification. It is not modified when updated. | -| admin_id | _Integer_ | This is the id of the admin who made this UserVerification. | -| user_id | _Integer_ | This is the id of the user the admin made this UserVerification for. | - -### AdminUpdate - -A lot of functionalities in the application require a user with admin rights. -The first user in database can make himself an admin. Every other user has to -be made admin by another admin. The admin rights can also be revoked. For every -change in a users admin rights, an AdminUpdate entry is made. The Admin update -table is used to verify whether the user is an admin by checking the is_admin -field in the latest entry related to the user. - -| Name | TYPE | Explanation | -| --------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The AdminUpdate id is unique and is used for identification in the application. It is created automatically with a new AdminUpdate. | -| timestamp | _DateTime_ | This is the date and time the AdminUpdate was created. It is created automatically with the new AdminUpdate. | -| user_id | _Integer_ | This is the id of the user whose admin status was updated. | -| admin_id | _Integer_ | This is the id of the admin who performed the update. | -| is_admin | _Boolean_ | Specifies whether the corresponding user is an admin (True) after the update or not (False). | - -### Uploads - -An admin can upload an image of a product to the application which is then -shown in the frontend. The UPLOAD_FOLDER can be set in configuration.py. There, -one can also specify the MAX_CONTENT_LENGTH and the valid file types via the -VALID_EXTENSIONS property. Through the uploads id, a product can be linked to -the Upload and the image. - -| Name | TYPE | Explanation | -| --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Upload id is unique and is used for identification in the application. It is created automatically with a new Upload. | -| timestamp | _DateTime_ | This is the date and time the Upload was created. It is created automatically with the new Upload. | -| admin_id | _Integer_ | This is the id of the admin who performed the Upload. | -| filename | _String(64)_ | This is the filename of the image that has been uploaded. It is saved in the UPLOAD_FOLDER and created automatically with the new Upload. | - -### Rank - -Depending on the rank, a User has can have different debt limits to his credit. - -| Name | TYPE | Explanation | -| -------------- | ------------ | --------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Rank id is unique and is used for Identification in the application. It is created automatically with a new Rank. | -| name | _String(32)_ | The Rank name is unique and is used for identification in the frontend. | -| debt_limit | _Integer_ | This specifies the debt limit a user with given Rank can have in his credit. | -| is_system_user | _Boolean_ | Specifies whether users with this rank are system users. | - -### RankUpdate - -When a user is verified, he has to be assigned a rank. Afterwards, the rank can -always be updated by an admin. Each time a users rank is set or changed, a -RankUpdate entry is made. To determine a users current rank, the rank_id field -is checked for the latest entry related to the user. - -| Name | TYPE | Explanation | -| --------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The RankUpdate id is unique and is used for identification in the application. It is created automatically with a new RankUpdate. | -| timestamp | _DateTime_ | This is the date and time the RankUpdate was created. It is created automatically with the new RankUpdate. | -| user_id | _Integer_ | This is the id of the user whose rank was updated. | -| admin_id | _Integer_ | This is the id of the who performed the update. | -| rank_id | _Integer_ | This is the id of the rank the user was updated to. | - -### Product - -Each item that can be sold through the application has to be a product. A -product can only be created by an admin. A product can have an image which is -shown in the frontend to identify it. In addition, each product has a price and -a pricehistory. Furthermore tags are used to group products into categories. - -| Name | TYPE | Explanation | -| --------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Product id is unique and is used for identification in the application. It is created automatically with a new Product. | -| creation_date | _DateTime_ | This is the date and time the Product was created. It is created automatically with the new Product. | -| created_by | _Integer_ | This is the id of the admin who created the product. | -| name | _String(64)_ | This is the name of the product used to identify it in the frontend. It has to be unique. | -| barcode | _String(32)_ | This saves the data represented by the products barcode. This entry is optional, but it has to be unique. | -| active | _Boolean_ | This indicates whether the product is active (True) and therefor available in the frontend or not (False). If not specified further, it will automatically be set to True. | -| countable | _Boolean_ | This indicates whether the product is countable (True) like a chocolate bar or not countable (False) like coffee powder. If not specified further, it will automatically be set to True. | -| revocable | _Boolean_ | This indicates whether the product is revocable (True) or not (False). If not specified further, it will automatically be set to True. | -| image_upload_id | _Integer_ | This is the id of the Upload with the products picture. This entry is optional. | - -### ProductPrice - -After a product was created, an admin has to set the products price, which he -can always update. Therefor, a ProductPrice entry is made. The products price -can then be determined by checking the price field of the latest entry related -to the product. In Addition, a pricehistory can be determined by listing the -id, timestamp and price of all entries related to the product. - -| Name | TYPE | Explanation | -| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The ProductPrice id is unique and is used for identification in the application. It is created automatically with a new ProductPrice. | -| timestamp | _DateTime_ | This is the date and time the Product was created. It is created automatically with the new Product. | -| product_id | _Integer_ | This is the id of the product whose price was set/changed. | -| admin_id | _Integer_ | This is the id of the admin who made this change in the products price. | -| price | _Integer_ | This is what the product price was set to. | - -### Tag - -A Tag can be assigned to each product. They help to sort products into -categories in the frontend. All products with the same tag are listed in the -same category. - -| Name | TYPE | Explanation | -| ----------- | ------------ | ------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Tag id is unique and is used for identification in the application. It is created automatically with a new Tag. | -| created_by | _Integer_ | This is the id of the admin who created the Tag. | -| name | _String(24)_ | This is the name of the Tag used to identify it in the frontend. It has to be unique. | -| is_for-sale | _Boolean_ | Specifies whether products with this tag are for sale. | - -### product_tag_assignments - -If a tag is added or removed from the product, a product_tag_assignments entry -is made or the corresponding entry is deleted. A product can have more than one -tag. The products tags can be determined by listing all tags from all entries -related to the product. - -| Name | TYPE | Explanation | -| ---------- | --------- | --------------------------------------------------- | -| product_id | _Integer_ | The id of the product the tag was assigned to. | -| tag_id | _Integer_ | The id of the tag that was assigned to the product. | - -### Purchase - -When a user buys a product, a Purchase entry is made. The user has to be -verified and the product has to be active. If the purchased product is -revocable, the purchase can be revoked, even more than once. So in addition, a -revokehistory for the purchase can be called. The price of the purchase is -calculated by multiplying the amount with the productprice. All prices of -purchases the user has made, which are not revoked, are added and withdrawn -from the users credit. Through adding the amounts a user has bought a specific -product, a list of the users favorite products can be created. - -| Name | TYPE | Explanation | -| ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Purchase id is unique and is used for identification in the application. It is created automatically with a new Purchase. | -| timestamp | _DateTime_ | This is the date and time the Product was created. It is created automatically with the new Product. | -| user_id | _Integer_ | This is the id of the user who made the purchase. The user has to be verified. | -| product_id | _Integer_ | This is the id of the product that has been purchased. The product has to be active | -| productprice | _Integer_ | This is the productprice when the purchase was made. It is determined automatically from the ProductPrice table when the purchase is created. | -| amount | _Integer_ | This describes the quantity in which the product was purchased. Even products which are not countable are sold in discreet amounts. | -| revoked | _Boolean_ | This indicates whether the Purchase is revoked (True) or not (False). If not specified further, it will automatically be set to False. The product has to be revocable. | - -### PurchaseRevoke - -If a purchase is revoked or re-revoked, a PurchaseRevoke entry is made. It is -used to determine the revokehistory of a purchase by listing the id, timestamp -and revoked field of each entry related to the purchase. - -| Name | TYPE | Explanation | -| ----------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The PurchaseRevoke id is unique and is used for identification in the application. It is created automatically with a new PurchaseRevoke. | -| timestamp | _DateTime_ | This is the date and time the PurchaseRevoke was created. It is created automatically with the new PurchaseRevoke. | -| purchase_id | _Integer_ | This is the id of the purchase the revoke was changed for. | -| revoked | _Boolean_ | This indicates whether the Purchase is revoked (True) or not (False). The product has to be revocable. | - -### ReplenishmentCollection - -When an admin fills up the products by buying them from a (system) user with -the communities money, he creates a ReplenishmentCollection entry. A -replenishmentcollection can be revoked, even more than once. So in addition, a -revokehistory for the replenishmentcollection can be called. When creating, the -admin has to pass a list of all single replenishments the -replenishmentcollection consists of. The price of a replenishmentcollection is -the sum of the total_price of all related non-revoked replenishments. This -price can be used to give an overview of the communities finances. - -| Name | TYPE | Explanation | -| --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The ReplenishmentCollection id is unique and is used for identification in the application. It is created automatically with a new ReplenishmentCollection. | -| timestamp | _DateTime_ | This is the date and time the ReplenishmentCollection was created. It is created automatically with the new ReplenishmentCollection. | -| admin_id | _Integer_ | This is the id of the admin who made the ReplenishmentCollection. | -| seller_id | _Integer_ | This is the id of the user from whom the products are purchased. | -| revoked | _Boolean_ | This indicates whether the ReplenishmentCollection is revoked (True) or not (False). If not specified further, it will automatically be set to False. | -| comment | _String(64)_ | This is a short comment where the admin explains what he bought and why. | - -### ReplenishmentCollectionRevoke - -If a replenishmentcollection is revoked or re-revoked by an admin, a -ReplenishmentCollectionRevoke entry is made. It is used to determine the -revokehistory of a replenishmentcollection by listing the id, timestamp and -revoked field of each entry related to the replenishmentcollection. - -| Name | TYPE | Explanation | -| ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The ReplenishmentCollectionRevoke id is unique and is used for identification in the application. It is created automatically with a new ReplenishmentCollectionRevoke. | -| timestamp | _DateTime_ | This is the date and time the ReplenishmentCollectionRevoke was created. It is created automatically with the new ReplenishmentCollectionRevoke. | -| admin_id | _Integer_ | This is the id of the admin who changed the revoke status. | -| replcoll_id | _Integer_ | This is the id of the replenishmentcollection where the revoked status has been changed. | -| revoked | _Boolean_ | This indicates whether the ReplenishmentCollection is revoked (True) or not (False). | - -### Replenishment - -A replenishment is a fill up of a single product and always has to be part of a -replenishmentcollection. It can be revoked. If all replenishments of a -replenishmentcollection are revoked, the replenishmentcollection is revoked -automatically. In this case, the replenishmentcollection can only be rerevoked -by rerevoking a replenishment. When rerevoking the replenishmentcollection, the -replenishments stay revoked. If not revoked, the replenishments total_price is -added to the price of the related replenishmentcollection. - -| Name | TYPE | Explanation | -| ----------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Replenishment id is unique and is used for identification in the application. It is created automatically with a new Replenishment. | -| replcoll_id | _Integer_ | This is the id of the replenishmentcollection this replenishment belongs to. | -| product_id | _Integer_ | This is the id of the product which is being refilled with this replenishment. | -| amount | _Integer_ | This describes the quantity in which the product is refilled. | -| total_price | _Integer_ | This is the price paid by the admin to an external seller, such as a supermarket, for this replenishment. | - -### Deposit - -If a user transfers money to the community, an admin has to create a deposit -for him. A deposit can be revoked, even more than once. So in addition, a -revokehistory for the deposit can be called. The amounts of all deposits -related to the user, which are not revoked, are added to the users credit. - -| Name | TYPE | Explanation | -| --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The Deposit id is unique and is used for identification in the application. It is created automatically with a new Deposit. | -| timestamp | _DateTime_ | This is the date and time the Deposit was created. It is created automatically with the new Deposit. | -| user_id | _Integer_ | This is the id of the user the deposit was made for. | -| admin_id | _Integer_ | This is the id of the admin who made the deposit. | -| amount | _Integer_ | This describes the amount (in cents) of the deposit. This is the money the user transferred to the community. | -| comment | _String(64)_ | This is a short comment where the admin explains what he did and why. | -| revoked | _Boolean_ | This indicates whether the Deposit is revoked (True) or not (False). If not specified further, it will automatically be set to False. | - -### DepositRevoke - -When an admin revokes or re-revokes a deposit, a DepositRevoke entry is made. It -is used to determine the revokehistory of a deposit by listing the id, timestamp -and revoked field of each entry related to the purchase. - -| Name | TYPE | Explanation | -| ---------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| id | _Integer_ | The DepositRevoke id is unique and is used for identification in the application. It is created automatically with a new DepositRevoke. | -| timestamp | _DateTime_ | This is the date and time the DepositRevoke was created. It is created automatically with the new DepositRevoke. | -| admin_id | _Integer_ | This is the id of the admin who changed the revoke status. | -| deposit_id | _Integer_ | This is the id of the deposit where the revoked status has been changed. | -| revoked | _Boolean_ | This indicates whether the Deposit is revoked (True) or not (False). | diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b924711 --- /dev/null +++ b/README.rst @@ -0,0 +1,630 @@ +shop-db2 +======== + +|Code style: black| |Imports: isort| |Average time to resolve an issue| |Percentage of issues still open| + +This is the documentation for shop-db. + +Table of content +---------------- + +1. `About shop.db <#about-shopdb>`__ +2. `Dependencies <#dependencies>`__ +3. `Getting started <#getting-started>`__ +4. `Development <#development>`__ +5. `Backups <#backups>`__ +6. `Unittests <#unittests>`__ +7. `Models <#models>`__ + +About shop.db +------------- + +We created shop.db in order to offer a simple way to manage purchases and user interactions in a small community. Over time, the project grew bigger and bigger and we decided to make it as flexible as possible, so that i can be used for more applications than our small shop service. Even if the development of shop.db has not progressed far enough to be called finished, we want to share the project so anyone can contribute and make shop.db better. In the following part, you can find a basic documentation for this project. + +shop.db can be used as a standalone backend and can be accessed via it’s API. Because this is not an elegant way to use this application, we developed the shop.db frontend, which can be found in it’s own repository: `shop-db2-frontend `__. Furthermore, the complete administration is carried out via the specially developed `shop-db2-react-admin `__ interface. + +Dependencies +------------ + +In order to use shop-db, you need to install the following main dependencies: + +Mandatory +~~~~~~~~~ + +- Python 3 (>= 3.7) +- Python 3 Virtual Environment +- pip3 +- git +- nginx + +.. code:: bash + + sudo apt install python3 python3-venv python3-pip git nginx + +Optional +~~~~~~~~ + +- wkhtmltopdf (For generating pdf templates) + +.. code:: bash + + sudo apt install wkhtmltopdf + +Getting started +--------------- + +Add an account for shop-db called shopdb_user. Since this account is only for running shop-db the extra arguments of -r is added to create a system account without creating a home directory: + +.. code:: bash + + sudo useradd -r shopdb_user + +Next we will create a directory for the installation of shop-db and change the owner to the shopdb_user account: + +.. code:: bash + + cd /srv + sudo git clone https://github.com/g3n35i5/shop-db2 + sudo chown -R shopdb_user:shopdb_user shop-db2 + +Now the Nginx server must be configured. Nginx installs a test site in this location that we wont’t need, so let’s remove it: + +.. code:: bash + + sudo rm /etc/nginx/sites-enabled/default + +Below you can see the Nginx configuration file for shop-db, which goes in /etc/nginx/sites-enabled/shop-db: + +.. code:: nginx + + server { + # listen on port 80 (http) + listen 80; + server_name shopdb; + location / { + # redirect any requests to the same URL but on https + return 301 https://$host$request_uri; + } + } + server { + # listen on port 443 (https) + listen 443 ssl; + server_name shopdb; + + # New: gzip compression + gzip on; + gzip_static on; + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + gzip_proxied any; + gzip_vary on; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + + # location of the SSL certificates + ssl_certificate /cert.pem; + ssl_certificate_key /key.pem; + + # write access and error logs to /var/log + access_log /var/log/shop-db_access.log; + error_log /var/log/shop-db_error.log; + + location / { + # forward application requests to the gunicorn server + proxy_pass http://localhost:; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + +Next up is to create and change to a virtual environment for shop-db. This will be done as the shopdb_user account: + +.. code:: bash + + sudo su -s /bin/bash shopdb_user + cd /srv/shop-db2 + python3 -m venv venv + source venv/bin/activate + # Use source venv/bin/activate.fish if you are using fish-shell + +Once you have activated the virtual environment you will notice the prompt change and then you can install the required python modules: + +.. code:: bash + + pip install -r requirements.txt + +Now the configuration file of shop-db2 has to be adjusted. Copy the ``configuration.example.py`` to the ``configuration.py`` file: + +.. code:: bash + + cp configuration.example.py configuration.py + +The most important change is the SECRET_KEY. This is later responsible for salting the user passwords and must be kept secret under all circumstances. Change this SECRET_KEY in the file ``configuration.py``. You can do this with a normal text editor or with the command ``sed``: + +.. code:: bash + + sed -i 's/YouWillNeverGuess/YOURBETTERSUPERSECRETKEY/g' configuration.py + +The first user (and at the same time the first administrator) as well as the default ranks are created using the ``setupdb.py`` script. Please look at the file and check whether the default settings for the ranks meet your requirements. + +If you are satisfied with them, you can now initialize the database: + +.. code:: bash + + python ./setupdb.py + +Ready? Almost. To start shop-db, you only have to type: + +.. code:: bash + + python ./wsgi.py + +However, so that the backend does not have to be started manually every time, it is advisable to run shop-db as a systemd service: + +.. code:: bash + + deactivate # To deactivate the virtual environment + exit # To switch back to the root user + sudo nano /etc/systemd/system/shop-db2@shopdb_user.service + +The file must have the following content: + +.. code:: ini + + [Unit] + Description=shop-db2 + After=network-online.target + + [Service] + Type=simple + User=%i + ExecStart=/srv/shop-db2/venv/bin/python3 /srv/shop-db2/wsgi.py + + [Install] + WantedBy=multi-user.target + +You need to reload systemd to make the daemon aware of the new configuration: + +.. code:: bash + + sudo systemctl --system daemon-reload + +To have shop-db start automatically at boot, enable the service: + +.. code:: bash + + sudo systemctl enable shop-db2@shopdb_user + +To disable the automatic start, use this command: + +.. code:: bash + + sudo systemctl disable shop-db2@shopdb_user + +To start shop-db now, use this command: + +.. code:: bash + + sudo systemctl start shop-db2@shopdb_user + +Development +----------- + +You want to start shop-db in developer mode and participate in the project? Great! The command + +.. code:: bash + + python ./shopdb.py --mode development + +starts shop-db for you in developer mode. This means that a temporary database is created in memory with default data defined in the dev folder. Your production database will not be used in this mode, but you should make sure you have a backup in case something goes wrong. + +Backups +------- + +To create backups from the database, you can use the ``backup.py`` script in the root directory of shop-db. To do this regularly, either a service or a crobjob can be used. + +Option 1: systemd service +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create two files with the following content: + +``/etc/systemd/system/shop-db-backup.service``: + +.. code:: ini + + [Unit] + Description=shop-db2 backup service + + [Service] + Type=oneshot + ExecStart=/srv/shop-db2/venv/bin/python /srv/shop-db2/backup.py + +``/etc/systemd/system/shop-db-backup.timer``: + +.. code:: ini + + [Unit] + Description=Timer for the shop-db2 backup service. + + [Timer] + OnCalendar=00/3 + + [Install] + WantedBy=timers.target + +Reload the services and start the backup service by typing + +.. code:: bash + + systemctl daemon-reload + systemctl start shop-db2-backup.timer + +If you want to check your timer and the states of the backups, you can use + +.. code:: bash + + systemctl list-timers --all + +Option 2: cronjob +~~~~~~~~~~~~~~~~~ + +Create the following cronob: + +.. code:: bash + + 0 */3 * * * /srv/shop-db2/backup.py + +Unittests +--------- + +Currently, most of the core features of shop-db are covered with the corresponding unittests. In order to execute them you can use the command + +.. code:: bash + + cd /srv/shop-db2 + source venv/bin/activate + ./run_tests_with_coverage.py --show-results + +Models +------ + +This section covers the models used in shop-db. They are defined in .shopdb/models.py + +User +~~~~ + +In order to interact with the database one needs some sort of user account which stores personal data, privileges and that can be referenced by other parts of the application. Therefor we use the User table. Anyone who can reach the shop-db application can create a user. After creating User through registering, one has to be verified by an admin to be able to use ones account. This prevents unauthorized use of the application. In addition a user can be an admin, has a rank, a credit to buy products and a list of favorite products. + ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NAME | TYPE | Explanation | ++=================+===============+===================================================================================================================================================================================================================================+ +| id | *Integer* | The user id is unique and is used to identify the user in the application. It is created automatically with the a new user. | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| creation_date | *DateTime* | This is the date and time the user was created. It is created automatically with the new user. It is not modified when user properties are updated. | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| firstname | *String(32)* | This is the users firstname. It is used to identify the user in the frontend. It does not have to be unique. It can be updated and changed after the users creation. | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| lastname | *String(32)* | This is the users lastname. It is used to identify the user in the frontend. It does not have to be unique. It can be updated and changed after the users creation. | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| password | *String(256)* | This is the password hash which is used to verify the users password when he logs in. It is automatically created from the password passed when creating or updating the user. The password itself is not stored in the database. | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| is_verified | *Boolean* | To prevent unauthorized access, each user has to be verified from an admin before he can carry out further actions. This column states whether the user is verified (True) or not (False). | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| image_upload_id | *Integer* | This is the id of the Upload with the user picture. This entry is optional. | ++-----------------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +UserVerification +~~~~~~~~~~~~~~~~ + +When a user is verified, a UserVerification entry is made. It is used to separate information about the verification from the user. As a result a user cant be verified twice and the verification date of a user can be called. A UserVerification can only be made by an admin. + ++-----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NAME | TYPE | Explanation | ++===========+============+=========================================================================================================================================================+ +| id | *Integer* | The UserVerification id is unique and is used for identification in the application. It is created automatically with a new UserVerification. | ++-----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the UserVerification was created. It is created automatically with the new UserVerification. It is not modified when updated. | ++-----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who made this UserVerification. | ++-----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+ +| user_id | *Integer* | This is the id of the user the admin made this UserVerification for. | ++-----------+------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+ + +AdminUpdate +~~~~~~~~~~~ + +A lot of functionalities in the application require a user with admin rights. The first user in database can make himself an admin. Every other user has to be made admin by another admin. The admin rights can also be revoked. For every change in a users admin rights, an AdminUpdate entry is made. The Admin update table is used to verify whether the user is an admin by checking the is_admin field in the latest entry related to the user. + ++-----------+------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++===========+============+=====================================================================================================================================+ +| id | *Integer* | The AdminUpdate id is unique and is used for identification in the application. It is created automatically with a new AdminUpdate. | ++-----------+------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the AdminUpdate was created. It is created automatically with the new AdminUpdate. | ++-----------+------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| user_id | *Integer* | This is the id of the user whose admin status was updated. | ++-----------+------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who performed the update. | ++-----------+------------+-------------------------------------------------------------------------------------------------------------------------------------+ +| is_admin | *Boolean* | Specifies whether the corresponding user is an admin (True) after the update or not (False). | ++-----------+------------+-------------------------------------------------------------------------------------------------------------------------------------+ + +Uploads +~~~~~~~ + +An admin can upload an image of a product to the application which is then shown in the frontend. The UPLOAD_FOLDER can be set in configuration.py. There, one can also specify the MAX_CONTENT_LENGTH and the valid file types via the VALID_EXTENSIONS property. Through the uploads id, a product can be linked to the Upload and the image. + ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++===========+==============+===========================================================================================================================================+ +| id | *Integer* | The Upload id is unique and is used for identification in the application. It is created automatically with a new Upload. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the Upload was created. It is created automatically with the new Upload. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who performed the Upload. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| filename | *String(64)* | This is the filename of the image that has been uploaded. It is saved in the UPLOAD_FOLDER and created automatically with the new Upload. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------+ + +Rank +~~~~ + +Depending on the rank, a User has can have different debt limits to his credit. + ++----------------+--------------+-----------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++================+==============+=======================================================================================================================+ +| id | *Integer* | The Rank id is unique and is used for Identification in the application. It is created automatically with a new Rank. | ++----------------+--------------+-----------------------------------------------------------------------------------------------------------------------+ +| name | *String(32)* | The Rank name is unique and is used for identification in the frontend. | ++----------------+--------------+-----------------------------------------------------------------------------------------------------------------------+ +| debt_limit | *Integer* | This specifies the debt limit a user with given Rank can have in his credit. | ++----------------+--------------+-----------------------------------------------------------------------------------------------------------------------+ +| is_system_user | *Boolean* | Specifies whether users with this rank are system users. | ++----------------+--------------+-----------------------------------------------------------------------------------------------------------------------+ + +RankUpdate +~~~~~~~~~~ + +When a user is verified, he has to be assigned a rank. Afterwards, the rank can always be updated by an admin. Each time a users rank is set or changed, a RankUpdate entry is made. To determine a users current rank, the rank_id field is checked for the latest entry related to the user. + ++-----------+------------+-----------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++===========+============+===================================================================================================================================+ +| id | *Integer* | The RankUpdate id is unique and is used for identification in the application. It is created automatically with a new RankUpdate. | ++-----------+------------+-----------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the RankUpdate was created. It is created automatically with the new RankUpdate. | ++-----------+------------+-----------------------------------------------------------------------------------------------------------------------------------+ +| user_id | *Integer* | This is the id of the user whose rank was updated. | ++-----------+------------+-----------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the who performed the update. | ++-----------+------------+-----------------------------------------------------------------------------------------------------------------------------------+ +| rank_id | *Integer* | This is the id of the rank the user was updated to. | ++-----------+------------+-----------------------------------------------------------------------------------------------------------------------------------+ + +Product +~~~~~~~ + +Each item that can be sold through the application has to be a product. A product can only be created by an admin. A product can have an image which is shown in the frontend to identify it. In addition, each product has a price and a pricehistory. Furthermore tags are used to group products into categories. + ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++=================+==============+==========================================================================================================================================================================================+ +| id | *Integer* | The Product id is unique and is used for identification in the application. It is created automatically with a new Product. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| creation_date | *DateTime* | This is the date and time the Product was created. It is created automatically with the new Product. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| created_by | *Integer* | This is the id of the admin who created the product. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| name | *String(64)* | This is the name of the product used to identify it in the frontend. It has to be unique. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| barcode | *String(32)* | This saves the data represented by the products barcode. This entry is optional, but it has to be unique. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| active | *Boolean* | This indicates whether the product is active (True) and therefor available in the frontend or not (False). If not specified further, it will automatically be set to True. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| countable | *Boolean* | This indicates whether the product is countable (True) like a chocolate bar or not countable (False) like coffee powder. If not specified further, it will automatically be set to True. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| revocable | *Boolean* | This indicates whether the product is revocable (True) or not (False). If not specified further, it will automatically be set to True. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| image_upload_id | *Integer* | This is the id of the Upload with the products picture. This entry is optional. | ++-----------------+--------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +ProductPrice +~~~~~~~~~~~~ + +After a product was created, an admin has to set the products price, which he can always update. Therefor, a ProductPrice entry is made. The products price can then be determined by checking the price field of the latest entry related to the product. In Addition, a pricehistory can be determined by listing the id, timestamp and price of all entries related to the product. + ++------------+------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++============+============+=======================================================================================================================================+ +| id | *Integer* | The ProductPrice id is unique and is used for identification in the application. It is created automatically with a new ProductPrice. | ++------------+------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the Product was created. It is created automatically with the new Product. | ++------------+------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| product_id | *Integer* | This is the id of the product whose price was set/changed. | ++------------+------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who made this change in the products price. | ++------------+------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| price | *Integer* | This is what the product price was set to. | ++------------+------------+---------------------------------------------------------------------------------------------------------------------------------------+ + +Tag +~~~ + +A Tag can be assigned to each product. They help to sort products into categories in the frontend. All products with the same tag are listed in the same category. + ++-------------+--------------+---------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++=============+==============+=====================================================================================================================+ +| id | *Integer* | The Tag id is unique and is used for identification in the application. It is created automatically with a new Tag. | ++-------------+--------------+---------------------------------------------------------------------------------------------------------------------+ +| created_by | *Integer* | This is the id of the admin who created the Tag. | ++-------------+--------------+---------------------------------------------------------------------------------------------------------------------+ +| name | *String(24)* | This is the name of the Tag used to identify it in the frontend. It has to be unique. | ++-------------+--------------+---------------------------------------------------------------------------------------------------------------------+ +| is_for-sale | *Boolean* | Specifies whether products with this tag are for sale. | ++-------------+--------------+---------------------------------------------------------------------------------------------------------------------+ + +product_tag_assignments +~~~~~~~~~~~~~~~~~~~~~~~ + +If a tag is added or removed from the product, a product_tag_assignments entry is made or the corresponding entry is deleted. A product can have more than one tag. The products tags can be determined by listing all tags from all entries related to the product. + +========== ========= =================================================== +Name TYPE Explanation +========== ========= =================================================== +product_id *Integer* The id of the product the tag was assigned to. +tag_id *Integer* The id of the tag that was assigned to the product. +========== ========= =================================================== + +Purchase +~~~~~~~~ + +When a user buys a product, a Purchase entry is made. The user has to be verified and the product has to be active. If the purchased product is revocable, the purchase can be revoked, even more than once. So in addition, a revokehistory for the purchase can be called. The price of the purchase is calculated by multiplying the amount with the productprice. All prices of purchases the user has made, which are not revoked, are added and withdrawn from the users credit. Through adding the amounts a user has bought a specific product, a list of the users favorite products can be created. + ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++==============+============+=========================================================================================================================================================================+ +| id | *Integer* | The Purchase id is unique and is used for identification in the application. It is created automatically with a new Purchase. | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the Product was created. It is created automatically with the new Product. | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| user_id | *Integer* | This is the id of the user who made the purchase. The user has to be verified. | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| product_id | *Integer* | This is the id of the product that has been purchased. The product has to be active | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| productprice | *Integer* | This is the productprice when the purchase was made. It is determined automatically from the ProductPrice table when the purchase is created. | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| amount | *Integer* | This describes the quantity in which the product was purchased. Even products which are not countable are sold in discreet amounts. | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| revoked | *Boolean* | This indicates whether the Purchase is revoked (True) or not (False). If not specified further, it will automatically be set to False. The product has to be revocable. | ++--------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +PurchaseRevoke +~~~~~~~~~~~~~~ + +If a purchase is revoked or re-revoked, a PurchaseRevoke entry is made. It is used to determine the revokehistory of a purchase by listing the id, timestamp and revoked field of each entry related to the purchase. + ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++=============+============+===========================================================================================================================================+ +| id | *Integer* | The PurchaseRevoke id is unique and is used for identification in the application. It is created automatically with a new PurchaseRevoke. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the PurchaseRevoke was created. It is created automatically with the new PurchaseRevoke. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| purchase_id | *Integer* | This is the id of the purchase the revoke was changed for. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------+ +| revoked | *Boolean* | This indicates whether the Purchase is revoked (True) or not (False). The product has to be revocable. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------+ + +ReplenishmentCollection +~~~~~~~~~~~~~~~~~~~~~~~ + +When an admin fills up the products by buying them from a (system) user with the communities money, he creates a ReplenishmentCollection entry. A replenishmentcollection can be revoked, even more than once. So in addition, a revokehistory for the replenishmentcollection can be called. When creating, the admin has to pass a list of all single replenishments the replenishmentcollection consists of. The price of a replenishmentcollection is the sum of the total_price of all related non-revoked replenishments. This price can be used to give an overview of the communities finances. + ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++===========+==============+=============================================================================================================================================================+ +| id | *Integer* | The ReplenishmentCollection id is unique and is used for identification in the application. It is created automatically with a new ReplenishmentCollection. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the ReplenishmentCollection was created. It is created automatically with the new ReplenishmentCollection. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who made the ReplenishmentCollection. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| seller_id | *Integer* | This is the id of the user from whom the products are purchased. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| revoked | *Boolean* | This indicates whether the ReplenishmentCollection is revoked (True) or not (False). If not specified further, it will automatically be set to False. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| comment | *String(64)* | This is a short comment where the admin explains what he bought and why. | ++-----------+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +ReplenishmentCollectionRevoke +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a replenishmentcollection is revoked or re-revoked by an admin, a ReplenishmentCollectionRevoke entry is made. It is used to determine the revokehistory of a replenishmentcollection by listing the id, timestamp and revoked field of each entry related to the replenishmentcollection. + ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++=============+============+=========================================================================================================================================================================+ +| id | *Integer* | The ReplenishmentCollectionRevoke id is unique and is used for identification in the application. It is created automatically with a new ReplenishmentCollectionRevoke. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the ReplenishmentCollectionRevoke was created. It is created automatically with the new ReplenishmentCollectionRevoke. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who changed the revoke status. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| replcoll_id | *Integer* | This is the id of the replenishmentcollection where the revoked status has been changed. | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| revoked | *Boolean* | This indicates whether the ReplenishmentCollection is revoked (True) or not (False). | ++-------------+------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +Replenishment +~~~~~~~~~~~~~ + +A replenishment is a fill up of a single product and always has to be part of a replenishmentcollection. It can be revoked. If all replenishments of a replenishmentcollection are revoked, the replenishmentcollection is revoked automatically. In this case, the replenishmentcollection can only be rerevoked by rerevoking a replenishment. When rerevoking the replenishmentcollection, the replenishments stay revoked. If not revoked, the replenishments total_price is added to the price of the related replenishmentcollection. + ++-------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++=============+===========+=========================================================================================================================================+ +| id | *Integer* | The Replenishment id is unique and is used for identification in the application. It is created automatically with a new Replenishment. | ++-------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| replcoll_id | *Integer* | This is the id of the replenishmentcollection this replenishment belongs to. | ++-------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| product_id | *Integer* | This is the id of the product which is being refilled with this replenishment. | ++-------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| amount | *Integer* | This describes the quantity in which the product is refilled. | ++-------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| total_price | *Integer* | This is the price paid by the admin to an external seller, such as a supermarket, for this replenishment. | ++-------------+-----------+-----------------------------------------------------------------------------------------------------------------------------------------+ + +Deposit +~~~~~~~ + +If a user transfers money to the community, an admin has to create a deposit for him. A deposit can be revoked, even more than once. So in addition, a revokehistory for the deposit can be called. The amounts of all deposits related to the user, which are not revoked, are added to the users credit. + ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++===========+==============+=======================================================================================================================================+ +| id | *Integer* | The Deposit id is unique and is used for identification in the application. It is created automatically with a new Deposit. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the Deposit was created. It is created automatically with the new Deposit. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| user_id | *Integer* | This is the id of the user the deposit was made for. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who made the deposit. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| amount | *Integer* | This describes the amount (in cents) of the deposit. This is the money the user transferred to the community. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| comment | *String(64)* | This is a short comment where the admin explains what he did and why. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ +| revoked | *Boolean* | This indicates whether the Deposit is revoked (True) or not (False). If not specified further, it will automatically be set to False. | ++-----------+--------------+---------------------------------------------------------------------------------------------------------------------------------------+ + +DepositRevoke +~~~~~~~~~~~~~ + +When an admin revokes or re-revokes a deposit, a DepositRevoke entry is made. It is used to determine the revokehistory of a deposit by listing the id, timestamp and revoked field of each entry related to the purchase. + ++------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| Name | TYPE | Explanation | ++============+============+=========================================================================================================================================+ +| id | *Integer* | The DepositRevoke id is unique and is used for identification in the application. It is created automatically with a new DepositRevoke. | ++------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| timestamp | *DateTime* | This is the date and time the DepositRevoke was created. It is created automatically with the new DepositRevoke. | ++------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| admin_id | *Integer* | This is the id of the admin who changed the revoke status. | ++------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| deposit_id | *Integer* | This is the id of the deposit where the revoked status has been changed. | ++------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------+ +| revoked | *Boolean* | This indicates whether the Deposit is revoked (True) or not (False). | ++------------+------------+-----------------------------------------------------------------------------------------------------------------------------------------+ + +.. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +.. |Imports: isort| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://pycqa.github.io/isort/ +.. |Average time to resolve an issue| image:: http://isitmaintained.com/badge/resolution/g3n35i5/shop-db2.svg + :target: http://isitmaintained.com/project/g3n35i5/shop-db2 +.. |Percentage of issues still open| image:: http://isitmaintained.com/badge/open/g3n35i5/shop-db2.svg + :target: http://isitmaintained.com/project/g3n35i5/shop-db2 \ No newline at end of file diff --git a/backup.py b/backup.py index f8a3966..0ede9ea 100755 --- a/backup.py +++ b/backup.py @@ -6,7 +6,7 @@ import sqlite3 import sys -from configuration import ProductiveConfig +from configuration import ProductiveConfig # isort: skip if __name__ == "__main__": _currentDate = datetime.datetime.now() diff --git a/configuration.example.py b/configuration.example.py index 8e80dba..dd6a95c 100644 --- a/configuration.example.py +++ b/configuration.example.py @@ -11,7 +11,7 @@ class BaseConfig(object): HOST = "127.0.0.1" PORT = 5000 SQLALCHEMY_TRACK_MODIFICATIONS = False - UPLOAD_FOLDER = PATH + "/shopdb/uploads/" + UPLOAD_FOLDER = PATH + "/src/shop_db2/uploads/" MAX_CONTENT_LENGTH = 4 * 1024 * 1024 VALID_EXTENSIONS = ["png"] MINIMUM_PASSWORD_LENGTH = 6 @@ -22,7 +22,7 @@ class ProductiveConfig(BaseConfig): DEBUG = False TEST = False ENV = "productive" - DATABASE_PATH = PATH + "/shopdb/shop.db" + DATABASE_PATH = PATH + "/src/shop_db2/shop.db" SQLALCHEMY_DATABASE_URI = "sqlite:///" + DATABASE_PATH diff --git a/dev/__init__.py b/dev/__init__.py index 65107e4..017267b 100644 --- a/dev/__init__.py +++ b/dev/__init__.py @@ -1,5 +1,5 @@ -from shopdb.api import bcrypt -from shopdb.models import * +from shop_db2.api import bcrypt +from shop_db2.models import * PASSWORD = bcrypt.generate_password_hash("1234") @@ -200,8 +200,6 @@ def insert_dev_data(db): timestamps.append(datetime.datetime.strptime(d, "%d.%m.%Y")) for i in range(4): - p = ProductPrice( - price=prices[i], product_id=1, admin_id=1, timestamp=timestamps[i] - ) + p = ProductPrice(price=prices[i], product_id=1, admin_id=1, timestamp=timestamps[i]) db.session.add(p) db.session.commit() diff --git a/migrations/env.py b/migrations/env.py index 4e77ca4..2305c1b 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -21,9 +21,7 @@ # target_metadata = mymodel.Base.metadata from flask import current_app -config.set_main_option( - "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") -) +config.set_main_option("sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI")) target_metadata = current_app.extensions["migrate"].db.metadata # other values from the config, defined by the needs of env.py, diff --git a/migrations/versions/0f534f276238_.py b/migrations/versions/0f534f276238_.py index 73aee48..3be17c4 100644 --- a/migrations/versions/0f534f276238_.py +++ b/migrations/versions/0f534f276238_.py @@ -5,6 +5,7 @@ Create Date: 2020-03-02 10:46:21.940195 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/3dc8453d444e_.py b/migrations/versions/3dc8453d444e_.py index 39d6732..3a52d68 100644 --- a/migrations/versions/3dc8453d444e_.py +++ b/migrations/versions/3dc8453d444e_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-28 17:04:04.967898 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/5e439efc0e2e_.py b/migrations/versions/5e439efc0e2e_.py index dceac93..56d36b4 100644 --- a/migrations/versions/5e439efc0e2e_.py +++ b/migrations/versions/5e439efc0e2e_.py @@ -5,6 +5,7 @@ Create Date: 2019-04-25 09:23:49.489179 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/8528842035bf_.py b/migrations/versions/8528842035bf_.py index 56df041..f7fe586 100644 --- a/migrations/versions/8528842035bf_.py +++ b/migrations/versions/8528842035bf_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-27 14:38:52.290160 """ + import sqlalchemy as sa from alembic import op @@ -33,10 +34,7 @@ def upgrade(): with op.batch_alter_table("replenishmentcollections") as batch_op: batch_op.alter_column("seller_id", existing_type=sa.INTEGER(), nullable=False) - print( - '\n\n!!! WARNING: column "seller_id" is set to 1 per default !!!\n' - "Please change these default values\n" - ) + print('\n\n!!! WARNING: column "seller_id" is set to 1 per default !!!\n' "Please change these default values\n") # ### end Alembic commands ### diff --git a/migrations/versions/854251ff744d_.py b/migrations/versions/854251ff744d_.py index 9f4cd29..797a32f 100644 --- a/migrations/versions/854251ff744d_.py +++ b/migrations/versions/854251ff744d_.py @@ -5,6 +5,7 @@ Create Date: 2019-03-26 09:19:09.007838 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/92b955138026_.py b/migrations/versions/92b955138026_.py index 317a166..1e4b07b 100644 --- a/migrations/versions/92b955138026_.py +++ b/migrations/versions/92b955138026_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-05 14:51:54.314055 """ + import sqlalchemy as sa from alembic import op @@ -19,9 +20,7 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column( "tags", - sa.Column( - "is_for_sale", sa.Boolean(), nullable=False, server_default=sa.true() - ), + sa.Column("is_for_sale", sa.Boolean(), nullable=False, server_default=sa.true()), ) # ### end Alembic commands ### diff --git a/migrations/versions/97bd09d1c2b5_.py b/migrations/versions/97bd09d1c2b5_.py index effc9d0..2ba2907 100644 --- a/migrations/versions/97bd09d1c2b5_.py +++ b/migrations/versions/97bd09d1c2b5_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-21 16:06:09.866020 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/a48d3896fd55_.py b/migrations/versions/a48d3896fd55_.py index 69603ac..ed30ab8 100644 --- a/migrations/versions/a48d3896fd55_.py +++ b/migrations/versions/a48d3896fd55_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-07 10:00:16.326372 """ + import sqlalchemy as sa from alembic import op @@ -19,9 +20,7 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column( "ranks", - sa.Column( - "is_system_user", sa.Boolean(), nullable=False, server_default=sa.false() - ), + sa.Column("is_system_user", sa.Boolean(), nullable=False, server_default=sa.false()), ) with op.batch_alter_table("ranks") as batch_op: batch_op.alter_column("debt_limit", existing_type=sa.INTEGER(), nullable=True) diff --git a/migrations/versions/e1f87046c5c1_.py b/migrations/versions/e1f87046c5c1_.py index 2971267..1d432ab 100644 --- a/migrations/versions/e1f87046c5c1_.py +++ b/migrations/versions/e1f87046c5c1_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-28 15:50:37.639242 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/ea05656e793c_.py b/migrations/versions/ea05656e793c_.py index a44a6af..5367f60 100644 --- a/migrations/versions/ea05656e793c_.py +++ b/migrations/versions/ea05656e793c_.py @@ -5,6 +5,7 @@ Create Date: 2019-03-26 09:23:49.235545 """ + import sqlalchemy as sa from alembic import op diff --git a/migrations/versions/f7782a0f2777_.py b/migrations/versions/f7782a0f2777_.py index 2f24515..c532abd 100644 --- a/migrations/versions/f7782a0f2777_.py +++ b/migrations/versions/f7782a0f2777_.py @@ -5,6 +5,7 @@ Create Date: 2020-02-20 10:02:22.842355 """ + import sqlalchemy as sa from alembic import op diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..015e010 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,129 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] +build-backend = "setuptools.build_meta" + +# Enables the usage of setuptools_scm +[tool.setuptools_scm] + + +[tool.pyright] +exclude = ["build/**"] + +[tool.isort] +profile = "black" +line_length = 120 +extend_skip_glob = ["venv*/*", "log/*", "migrations/*"] + +[tool.black] +line_length = 120 +extend-exclude = "venv.*|migrations.*" + +[tool.ruff] +line-length = 120 +src = ["src"] +lint.select = ["C901", "D"] +extend-exclude = ["log", "migrations"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "D103", # Missing docstring in public function +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.pylint.master] +recursive = "yes" +# The following settings modify the behavior when running pylint with pylint.extensions.docparams plugin loaded +accept-no-param-doc = "no" +accept-no-raise-doc = "no" +accept-no-return-doc = "no" +accept-no-yields-doc = "no" +ignore = [ + "conf.py", # The Sphinx config file + "build", + "dist", + "log", + "migrations", +] +ignore-patterns = [ + "venv.*", + "^\\..+", # Ignore hidden folders or files + "migrations.*", +] + +[tool.pylint.messages_control] +max-line-length = 120 +# https://github.com/samuelcolvin/pydantic/issues/1961 +extension-pkg-whitelist = "pydantic" +# ignore unrecognized-option because of https://github.com/PyCQA/pylint/issues/6799 +disable = """ + unrecognized-option, + too-few-public-methods, + logging-fstring-interpolation, + too-many-instance-attributes, + too-many-arguments, + missing-function-docstring + """ + +[tool.pylint.similarities] +# Exclude the following from code duplication checks +ignore-comments = "yes" +ignore-docstrings = "yes" +ignore-imports = "yes" +ignore-signatures = "yes" + +[tool.mypy] +# Functions need to be annotated +disallow_untyped_defs = true +warn_unused_ignores = true +exclude = [ + "shop-db2-\\d+", # Ignore temporary folder created by setuptools when building an sdist + "venv.*/", + "log/", + "build/", + "dist/", + "migrations/", +] + +[tool.pytest.ini_options] +addopts = """ + -vv + --doctest-modules + --import-mode=append + --ignore-glob=shop-db2-[0-9]* + --ignore="migrations" + --ignore="configuration.example.py" + --ignore="docs/_scripts" + --cov=shop_db2 + --cov-config=pyproject.toml + --cov-report= + """ + +[tool.coverage.run] +branch = true + +[tool.coverage.paths] +# Maps coverage measured in site-packages to source files in src +source = ["src/", ".tox/*/lib/python*/site-packages/"] + +[tool.coverage.report] +exclude_also = ["\\.\\.\\.", "if TYPE_CHECKING:"] +partial_branches = ["pragma: no branch", "if not TYPE_CHECKING:"] + +[tool.coverage.html] +directory = "reports/coverage_html" + +[tool.coverage.xml] +output = "reports/coverage.xml" + +[[tool.mypy.overrides]] +module = [ + # Ignore packages that do not provide type hints here + # For example, add "dash.*" to ignore all imports from Dash + "sphinxawesome_theme.postprocess", + "sqlalchemy.*", + "flask_script.*", + "flask_migrate.*", +] +ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1297f4e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -black==22.3.0 -coverage==4.5.4 -flask-bcrypt==0.7.1 -flask-cors==3.0.10 -flask-migrate==2.5.2 -flask-script==2.0.6 -flask-testing==0.7.1 -gunicorn==19.9.0 -isort==5.10.1 -nose==1.3.7 -pdfkit==0.6.1 -pillow==9.1.1 -pip-chill==1.0.1 -pyfakefs==3.6 -pyjwt==1.7.1 diff --git a/run_tests_with_coverage.py b/run_tests_with_coverage.py deleted file mode 100755 index 4c2feb0..0000000 --- a/run_tests_with_coverage.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -__author__ = "g3n35i5" - -import os -import shutil -import sys -import threading -import time -import unittest -import webbrowser -from argparse import ArgumentParser -from http.server import CGIHTTPRequestHandler, HTTPServer - -import configuration as config - -try: - import coverage -except ImportError: - raise ImportError( - "Please install the coverage module (https://pypi.python.org/pypi/coverage/)" - ) - - -class SilentHTTPHandler(CGIHTTPRequestHandler): - """ - Custom HTTP handler which suppresses the console output. - """ - - def log_message(self, format, *args): - return - - -def start_server(path, port=8000) -> None: - """ - Start a simple webserver serving path on port - - :param path: Root path for the webserver - :param port: Port for the webserver - :return: None - """ - os.chdir(path) - httpd = HTTPServer(("", port), SilentHTTPHandler) - httpd.serve_forever() - - -def main(args) -> None: - # Load .coveragerc.ini configuration file - config_file = os.path.join(config.PATH, ".coveragerc.ini") - os.environ["COVERAGE_PROCESS_START"] = config_file - cov = coverage.coverage(config_file=config_file) - - # Start coverage - cov.start() - html_cov_path = os.path.join(config.PATH, "htmlcov") - webserver_port = 8000 - try: - tests = unittest.TestLoader().discover(os.path.join(config.PATH, "tests")) - unittest.TextTestRunner(verbosity=2).run(tests) - finally: - cov.stop() - cov.save() - cov.combine() - if os.path.exists(html_cov_path): - shutil.rmtree(html_cov_path) - cov.html_report(directory=html_cov_path) - cov.xml_report() - - if args.show_results is False: - return - - # Start webserver - daemon = threading.Thread( - name="Coverage Server", - target=start_server, - args=(html_cov_path, webserver_port), - ) - daemon.setDaemon(True) - daemon.start() - - # Open page in browser - webbrowser.open("http://localhost:{}".format(webserver_port)) - - # Keep the script alive as long it - print( - "Webserver is running on port {}. Press CTRL+C to exit".format(webserver_port) - ) - while True: - try: - time.sleep(0.1) - except KeyboardInterrupt: - sys.exit(0) - - -if __name__ == "__main__": - parser = ArgumentParser(description="Running unittests for shop-db2 with coverage") - parser.add_argument( - "--show-results", help="Open results in web browser", action="store_true" - ) - args = parser.parse_args() - main(args) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..089fd0a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,102 @@ +[metadata] +name = shop-db2 +author = Jan-Frederik Schmidt +author_email = janfschmidt@mailbox.org +description = The simple way to manage purchases and user interactions in a small community. +long_description = file:README.rst +long_description_content_type = text/x-rst +license = MIT +url = https://github.com/g3n35i5/shop-db2 + + +[options] +python_requires = >=3.8,<3.10 +packages = find: +package_dir = + =src +install_requires = + importlib-metadata + bcrypt==3.1.7 + Flask==1.1.1 + Flask-Bcrypt==0.7.1 + Flask-Cors==3.0.8 + Flask-Migrate==2.5.2 + Flask-Script==2.0.6 + Flask-SQLAlchemy==2.4.0 + gunicorn==19.9.0 + itsdangerous==1.1.0 + Jinja2==2.10.1 + Mako==1.1.0 + MarkupSafe==1.1.1 + nose==1.3.7 + pdfkit==0.6.1 + Pillow==10.3.0 + pycparser==2.19 + PyJWT==1.7.1 + python-dateutil==2.8.0 + python-editor==1.0.4 + six==1.12.0 + SQLAlchemy==1.3.8 + Werkzeug==0.15.6 + + +[options.packages.find] +where = src + +[options.package_data] +* = py.typed + +shop_db2 = + uploads/** + templates/** + +[options.extras_require] +dev = + %(tox)s + %(lint)s + %(test)s + %(build)s + +lint = + %(lint-template)s + # Add your linting dependencies below this line + +test = + %(test-template)s + # Add your testing dependencies below this line. + # Dependencies that are imported in one of your files + # must also be added to the linting dependencies. + pyfakefs==3.6 + Flask-Testing==0.7.1 + +build = + %(build-template)s + # Add your build dependencies below this line + + +########################################## +# DO NOT CHANGE ANYTHING BELOW THIS LINE # +########################################## + +tox = + tox==4.14.2 + +lint-template = + isort==5.13.2 + black==24.3.0 + mypy==1.9.0 + pylint==3.1.0 + pytest==8.1.1 + types-docutils + types-setuptools + jinja2==2.10.1 + ruff==0.3.5 + +test-template = + pytest==8.1.1 + pytest-randomly==3.15.0 + pytest-cov==5.0.0 + coverage[toml]==7.4.4 + +build-template = + build[virtualenv]==1.2.1 diff --git a/setupdb.py b/setupdb.py index ce9a8b7..7792599 100644 --- a/setupdb.py +++ b/setupdb.py @@ -9,16 +9,16 @@ from sqlalchemy.exc import IntegrityError -import configuration as config -import shopdb.exceptions as exc -from shopdb.api import app, db, set_app -from shopdb.helpers.users import insert_user -from shopdb.models import Rank, User +import shop_db2.exceptions as exc +from shop_db2.api import app, db, set_app +from shop_db2.helpers.users import insert_user +from shop_db2.models import Rank, User + +import configuration as config # isort: skip def _get_password(): - """ - Ask the user for a password and repeat it until both passwords match and + """Ask the user for a password and repeat it until both passwords match and are not empty. :return: The password as plaintext. @@ -39,8 +39,7 @@ def _get_password(): def input_user(): - """ - Prompts the user to enter the name and lastname of the first user + """Prompts the user to enter the name and lastname of the first user (administrator) and then his password. :return: The firstname, the lastname and the password. diff --git a/shopdb.py b/shopdb.py deleted file mode 100755 index edc19d9..0000000 --- a/shopdb.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -__author__ = "g3n35i5" - -import sys - -try: - import configuration as config -except ModuleNotFoundError: - sys.exit( - "No configuration file was found. Please make sure, " - "that you renamed or copied the sample configuration " - "configuration.example.py and adapted it to your needs." - ) - -import argparse - -from flask_cors import CORS - -from dev import insert_dev_data -from shopdb.api import app, db, set_app - -parser = argparse.ArgumentParser(description="Starting script shop.db") -parser.add_argument("--mode", choices=["development", "local"]) -args = parser.parse_args() - -if args.mode == "development": - print("Starting shop-db in developing mode") - set_app(config.DevelopmentConfig) - app.app_context().push() - db.create_all() - insert_dev_data(db) - -elif args.mode == "local": - print("Starting shop-db on the local database") - set_app(config.ProductiveConfig) - CORS(app, expose_headers="*") - app.app_context().push() - -else: - parser.print_help() - sys.exit(f"{args.mode}: invalid operating mode") - -app.run(host=app.config["HOST"], port=app.config["PORT"]) diff --git a/shopdb_entry.py b/shopdb_entry.py new file mode 100755 index 0000000..be7ca39 --- /dev/null +++ b/shopdb_entry.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +__author__ = "g3n35i5" + +import sys + +try: + import configuration as config # isort: skip +except ModuleNotFoundError: + sys.exit( + "No configuration file was found. Please make sure, " + "that you renamed or copied the sample configuration " + "configuration.example.py and adapted it to your needs." + ) + +import argparse + +from flask_cors import CORS + +from dev import insert_dev_data +from shop_db2.api import app, db, set_app + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Starting script shop.db") + parser.add_argument("--mode", choices=["development", "local"]) + args = parser.parse_args() + + if args.mode == "development": + print("Starting shop-db in developing mode") + set_app(config.DevelopmentConfig) + app.app_context().push() + db.create_all() + insert_dev_data(db) + + elif args.mode == "local": + print("Starting shop-db on the local database") + set_app(config.ProductiveConfig) + CORS(app, expose_headers="*") + app.app_context().push() + + else: + parser.print_help() + sys.exit(f"{args.mode}: invalid operating mode") + + app.run(host=app.config["HOST"], port=app.config["PORT"]) diff --git a/src/shop_db2/__init__.py b/src/shop_db2/__init__.py new file mode 100644 index 0000000..0660b39 --- /dev/null +++ b/src/shop_db2/__init__.py @@ -0,0 +1,33 @@ +"""The simple way to manage purchases and user interactions in a small community.""" + +from importlib_metadata import PackageNotFoundError as _PackageNotFoundError +from importlib_metadata import version as _version + +__module_name__ = "shop_db2" + +try: # pragma: no cover + __version__ = _version(__module_name__) +except _PackageNotFoundError as error: # pragma: no cover + raise ModuleNotFoundError( + f"Unable to determine version of package '{__module_name__}'. " + "If you are on a local development system, use 'pip install -e .[dev]' in order to install the package. " + "If you are on a productive system, this shouldn't happen. Please report a bug." + ) from error + + +def get_app_name() -> str: + """Returns the human-readable and prettified name of the application. + + Returns: + The name of the application. + """ + return __module_name__.replace("_", "-") + + +def get_app_version() -> str: + """Returns the version of the application. + + Returns: + The version of the application. + """ + return __version__ diff --git a/shopdb/api.py b/src/shop_db2/api.py similarity index 65% rename from shopdb/api.py rename to src/shop_db2/api.py index 2b4169c..4de750f 100644 --- a/shopdb/api.py +++ b/src/shop_db2/api.py @@ -5,8 +5,9 @@ from flask import Flask, jsonify from flask_bcrypt import Bcrypt -import configuration as config -from shopdb.shared import db +from shop_db2.shared import db + +import configuration as config # isort: skip app = Flask(__name__) @@ -21,8 +22,7 @@ def set_app(configuration): - """ - Sets all parameters of the applications to those defined in the dictionary + """Sets all parameters of the applications to those defined in the dictionary "configuration" and returns the application object. :param configuration: The dictionary with all settings for the application @@ -35,8 +35,7 @@ def set_app(configuration): @app.route("/", methods=["GET"]) def index(): - """ - A route that simply returns that the backend is online. + """A route that simply returns that the backend is online. :return: A message which says that the backend is online. """ @@ -47,49 +46,64 @@ def index(): # Error handler # noinspection PyUnresolvedReferences -import shopdb.helpers.errors # noqa: E402 +import shop_db2.helpers.errors # noqa: E402 + # App hooks # noinspection PyUnresolvedReferences -import shopdb.helpers.hooks # noqa: E402 +import shop_db2.helpers.hooks # noqa: E402 + # Backup routes # noinspection PyUnresolvedReferences -import shopdb.routes.backups # noqa: E402 +import shop_db2.routes.backups # noqa: E402 + # Deposit routes # noinspection PyUnresolvedReferences -import shopdb.routes.deposits # noqa: E402 +import shop_db2.routes.deposits # noqa: E402 + # Financial overview route # noinspection PyUnresolvedReferences -import shopdb.routes.financial_overview # noqa: E402 +import shop_db2.routes.financial_overview # noqa: E402 + # Image routes # noinspection PyUnresolvedReferences -import shopdb.routes.images # noqa: E402 +import shop_db2.routes.images # noqa: E402 + # Login route # noinspection PyUnresolvedReferences -import shopdb.routes.login # noqa: E402 +import shop_db2.routes.login # noqa: E402 + # Maintenance routes # noinspection PyUnresolvedReferences -import shopdb.routes.maintenance # noqa: E402 +import shop_db2.routes.maintenance # noqa: E402 + # Product routes # noinspection PyUnresolvedReferences -import shopdb.routes.products # noqa: E402 +import shop_db2.routes.products # noqa: E402 + # Purchase routes # noinspection PyUnresolvedReferences -import shopdb.routes.purchases # noqa: E402 +import shop_db2.routes.purchases # noqa: E402 + # Rank routes # noinspection PyUnresolvedReferences -import shopdb.routes.ranks # noqa: E402 +import shop_db2.routes.ranks # noqa: E402 + # ReplenishmentCollection routes # noinspection PyUnresolvedReferences -import shopdb.routes.replenishmentcollections # noqa: E402 +import shop_db2.routes.replenishmentcollections # noqa: E402 + # StocktakingCollection routes # noinspection PyUnresolvedReferences -import shopdb.routes.stocktakingcollections # noqa: E402 +import shop_db2.routes.stocktakingcollections # noqa: E402 + # Tag assignment routes # noinspection PyUnresolvedReferences -import shopdb.routes.tagassignments # noqa: E402 +import shop_db2.routes.tagassignments # noqa: E402 + # Tag routes # noinspection PyUnresolvedReferences -import shopdb.routes.tags # noqa: E402 +import shop_db2.routes.tags # noqa: E402 + # User routes # noinspection PyUnresolvedReferences -import shopdb.routes.users # noqa: E402 +import shop_db2.routes.users # noqa: E402 diff --git a/shopdb/exceptions.py b/src/shop_db2/exceptions.py similarity index 96% rename from shopdb/exceptions.py rename to src/shop_db2/exceptions.py index fb71348..cc83008 100644 --- a/shopdb/exceptions.py +++ b/src/shop_db2/exceptions.py @@ -215,9 +215,7 @@ class UserAlreadyVerified(ShopdbException): class UserNeedsPassword(ShopdbException): type = "error" - message = ( - "The user must first set a password before he can become " "an administrator." - ) + message = "The user must first set a password before he can become " "an administrator." code = 401 @@ -267,8 +265,5 @@ class TokenHasExpired(ShopdbException): class NoRemainingAdmin(ShopdbException): type = "error" - message = ( - "There always has to be at least one admin. You cant remove" - "your admin privileges" - ) + message = "There always has to be at least one admin. You cant remove" "your admin privileges" code = 401 diff --git a/shopdb/__init__.py b/src/shop_db2/helpers/__init__.py similarity index 100% rename from shopdb/__init__.py rename to src/shop_db2/helpers/__init__.py diff --git a/shopdb/helpers/decorators.py b/src/shop_db2/helpers/decorators.py similarity index 92% rename from shopdb/helpers/decorators.py rename to src/shop_db2/helpers/decorators.py index 003113e..44f39b9 100644 --- a/shopdb/helpers/decorators.py +++ b/src/shop_db2/helpers/decorators.py @@ -7,14 +7,13 @@ import jwt from flask import Response, request -import shopdb.exceptions as exc -from shopdb.api import app -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import app +from shop_db2.models import User def checkIfUserIsValid(f): - """ - This function checks whether the requested user exists, has been verified and is active. + """This function checks whether the requested user exists, has been verified and is active. If this is not the case the request will be blocked. :param f: Is the wrapped function. @@ -47,8 +46,7 @@ def decorator(*args, **kwargs): def adminRequired(f): - """ - This function checks whether a valid token is contained in the request. + """This function checks whether a valid token is contained in the request. If this is not the case, or the user has no admin rights, the request will be blocked. @@ -103,8 +101,7 @@ def decorated(*args, **kwargs): def adminOptional(f): - """ - This function checks whether a valid token is contained in the request. + """This function checks whether a valid token is contained in the request. If this is not the case, or the user has no admin rights, the following function returns only a part of the available data. @@ -155,8 +152,7 @@ def decorated(*args, **kwargs): def deprecate_route(message=""): - """ - This decorator adds a warning message to the response header when the route is marked as deprecated. + """This decorator adds a warning message to the response header when the route is marked as deprecated. :param message: The message to be added to the response header. """ diff --git a/shopdb/helpers/deposits.py b/src/shop_db2/helpers/deposits.py similarity index 84% rename from shopdb/helpers/deposits.py rename to src/shop_db2/helpers/deposits.py index eabc7b0..d35dcf3 100644 --- a/shopdb/helpers/deposits.py +++ b/src/shop_db2/helpers/deposits.py @@ -4,15 +4,14 @@ from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Deposit, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Deposit, User def insert_deposit(data, admin): - """ - This help function creates a new deposit with the given data. + """This help function creates a new deposit with the given data. :raises DataIsMissing: If not all required data is available. :raises WrongType: If one or more data is of the wrong type. @@ -21,7 +20,6 @@ def insert_deposit(data, admin): :raises InvalidAmount: If amount is equal to zero. :raises CouldNotCreateEntry: If any other error occurs. """ - required = {"user_id": int, "amount": int, "comment": str} check_fields_and_types(data, required) diff --git a/shopdb/helpers/errors.py b/src/shop_db2/helpers/errors.py similarity index 91% rename from shopdb/helpers/errors.py rename to src/shop_db2/helpers/errors.py index 27f229c..1227a7e 100644 --- a/shopdb/helpers/errors.py +++ b/src/shop_db2/helpers/errors.py @@ -5,14 +5,13 @@ import werkzeug.exceptions as werkzeug_exceptions from flask import jsonify -import shopdb.exceptions as exc -from shopdb.api import app, db +import shop_db2.exceptions as exc +from shop_db2.api import app, db @app.errorhandler(Exception) def handle_error(error): - """ - This wrapper catches all exceptions and, if possible, returns a user + """This wrapper catches all exceptions and, if possible, returns a user friendly response. Otherwise, it will raise the error :param error: Is the exception to be raised. diff --git a/shopdb/helpers/hooks.py b/src/shop_db2/helpers/hooks.py similarity index 79% rename from shopdb/helpers/hooks.py rename to src/shop_db2/helpers/hooks.py index 3521353..cac60e9 100644 --- a/shopdb/helpers/hooks.py +++ b/src/shop_db2/helpers/hooks.py @@ -8,16 +8,15 @@ from flask import g, request -import shopdb.exceptions as exc -from shopdb.api import app -from shopdb.helpers.decorators import adminOptional +import shop_db2.exceptions as exc +from shop_db2.api import app +from shop_db2.helpers.decorators import adminOptional @app.before_request @adminOptional def before_request_hook(admin): - """ - This function is executed before each request is processed. Its purpose is + """This function is executed before each request is processed. Its purpose is to check whether the application is currently in maintenance mode. If this is the case, the current request is aborted and a corresponding exception is raised. @@ -33,7 +32,6 @@ def before_request_hook(admin): :raises MaintenanceMode: if the application is in maintenance mode. """ - # Debug timer g.start = time.time() @@ -54,8 +52,7 @@ def before_request_hook(admin): @app.after_request def after_request_hook(response): - """ - This functions gets executed each time a request is finished. + """This functions gets executed each time a request is finished. :param response: is the response to be returned. :return: The request response. @@ -63,10 +60,6 @@ def after_request_hook(response): # If the app is in DEBUG mode, log the request execution time if app.logger.level == logging.DEBUG: execution_time = datetime.timedelta(seconds=(time.time() - g.start)) - app.logger.debug( - "Request execution time for '{}': {}".format( - request.endpoint, execution_time - ) - ) + app.logger.debug("Request execution time for '{}': {}".format(request.endpoint, execution_time)) return response diff --git a/shopdb/helpers/products.py b/src/shop_db2/helpers/products.py similarity index 83% rename from shopdb/helpers/products.py rename to src/shop_db2/helpers/products.py index 66c4d23..465c206 100644 --- a/shopdb/helpers/products.py +++ b/src/shop_db2/helpers/products.py @@ -5,16 +5,15 @@ from sqlalchemy import and_ -import shopdb.exceptions as exc -import shopdb.helpers.purchases as purchase_helpers -import shopdb.helpers.replenishments as replenishment_helpers -import shopdb.helpers.stocktakings as stocktaking_helpers -from shopdb.models import Product, ProductPrice +import shop_db2.exceptions as exc +import shop_db2.helpers.purchases as purchase_helpers +import shop_db2.helpers.replenishments as replenishment_helpers +import shop_db2.helpers.stocktakings as stocktaking_helpers +from shop_db2.models import Product, ProductPrice def _shift_date_to_begin_of_day(date): - """ - This function moves a timestamp to the beginning of the day. + """This function moves a timestamp to the beginning of the day. :param date: Is the date to be moved. :return: Is the shifted date. @@ -23,8 +22,7 @@ def _shift_date_to_begin_of_day(date): def _shift_date_to_end_of_day(date): - """ - This function moves a timestamp to the end of the day. + """This function moves a timestamp to the end of the day. :param date: Is the date to be moved. :return: Is the shifted date. @@ -33,15 +31,13 @@ def _shift_date_to_end_of_day(date): def _get_product_mean_price_in_time_range(product_id, start, end): - """ - This function calculates the mean price in a given range of time. + """This function calculates the mean price in a given range of time. :param product_id: Is the product id. :param start: Is the start date. :param end: Is the end date. :return: The mean product price in the given time range. """ - # Check if start and end dates are date objects. if not all([isinstance(d, datetime.datetime) for d in [start, end]]): raise exc.InvalidData() @@ -109,27 +105,21 @@ def _get_product_mean_price_in_time_range(product_id, start, end): day_count += 1 sum_price += current_price if current_date in list(map(lambda x: x.timestamp, changes)): - current_price = next( - item for item in changes if item.timestamp == current_date - ).price + current_price = next(item for item in changes if item.timestamp == current_date).price # Return the mean product price as integer. return int(round(sum_price / day_count)) def get_theoretical_stock_of_product(product_id: int) -> int: - """ - Returns the theoretical stock level of a product. + """Returns the theoretical stock level of a product. The theoretical stock level of a product is the result of the number determined in the last stocktaking minus the sum of the amount of purchases that were not revoked plus the sum of the amount of replenishments since then. """ - # Get the latest stocktaking of a product - latest_stocktaking = stocktaking_helpers.get_latest_stocktaking_of_product( - product_id - ) + latest_stocktaking = stocktaking_helpers.get_latest_stocktaking_of_product(product_id) # If there has been a stocktaking, it defines the start timestamp. # Otherwise, we have to take all purchases of this product into account. @@ -143,16 +133,10 @@ def get_theoretical_stock_of_product(product_id: int) -> int: end = datetime.datetime.utcnow() # Get the sum of all purchase amounts in the selected interval - sum_purchase_amount = purchase_helpers.get_purchase_amount_in_interval( - product_id, start, end - ) + sum_purchase_amount = purchase_helpers.get_purchase_amount_in_interval(product_id, start, end) # Get the sum of all refund amounts in the selected interval - sum_replenishment_amount = ( - replenishment_helpers.get_replenishment_amount_in_interval( - product_id, start, end - ) - ) + sum_replenishment_amount = replenishment_helpers.get_replenishment_amount_in_interval(product_id, start, end) # Theoretical stock level return stocktaking_count - sum_purchase_amount + sum_replenishment_amount diff --git a/shopdb/helpers/purchases.py b/src/shop_db2/helpers/purchases.py similarity index 84% rename from shopdb/helpers/purchases.py rename to src/shop_db2/helpers/purchases.py index 029f585..d4d4cc6 100644 --- a/shopdb/helpers/purchases.py +++ b/src/shop_db2/helpers/purchases.py @@ -8,20 +8,15 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.sql import func -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.helpers.utils import parse_timestamp -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Product, Purchase, Rank, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.helpers.utils import parse_timestamp +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Product, Purchase, Rank, User -def get_purchase_amount_in_interval( - product_id: int, start: datetime.datetime, end: datetime.datetime -) -> int: - """ - Returns the sum of the amount of all purchases of the given product. - """ - +def get_purchase_amount_in_interval(product_id: int, start: datetime.datetime, end: datetime.datetime) -> int: + """Returns the sum of the amount of all purchases of the given product.""" result = ( db.session.query(func.sum(Purchase.amount)) .filter(Purchase.product_id == product_id) @@ -34,8 +29,7 @@ def get_purchase_amount_in_interval( def insert_purchase(admin: Optional[User], data: dict) -> None: - """ - This helper function creates a single purchase without doing a commit. + """This helper function creates a single purchase without doing a commit. :param admin: Is the administrator user, determined by @adminOptional. :param data: Is the purchase data. diff --git a/shopdb/helpers/query.py b/src/shop_db2/helpers/query.py similarity index 84% rename from shopdb/helpers/query.py rename to src/shop_db2/helpers/query.py index 5641b12..0fecd82 100644 --- a/shopdb/helpers/query.py +++ b/src/shop_db2/helpers/query.py @@ -10,16 +10,14 @@ from sqlalchemy.sql.expression import BinaryExpression from werkzeug import ImmutableMultiDict -import shopdb.exceptions as exc -from shopdb.api import db +import shop_db2.exceptions as exc +from shop_db2.api import db class QueryFromRequestParameters: __valid_fields_and_types__ = {"filter": dict, "pagination": dict, "sort": dict} - def __init__( - self, model: db.Model, arguments: ImmutableMultiDict, fields=List[str] - ) -> None: + def __init__(self, model: db.Model, arguments: ImmutableMultiDict, fields=List[str]) -> None: # Database table self._model: db.Model = model # Column mapper for validation and column access @@ -37,8 +35,7 @@ def __init__( self._query = db.session.query(self._model) def _parse_arguments(self) -> None: - """ - This function parses the input ImmutableMultiDict into a vanilla python dictionary. + """This function parses the input ImmutableMultiDict into a vanilla python dictionary. All key, value pairs are processed with the json.loads() method to correctly load lists and dictionaries in string representation. @@ -75,9 +72,7 @@ def _parse_arguments(self) -> None: try: parsed_value = json.loads(argument_value) # The type must be correct - assert isinstance( - parsed_value, self.__valid_fields_and_types__.get(argument_key) - ) + assert isinstance(parsed_value, self.__valid_fields_and_types__.get(argument_key)) parsed_arguments[argument_key] = parsed_value except (AssertionError, json.decoder.JSONDecodeError): raise exc.InvalidQueryParameters() @@ -86,8 +81,7 @@ def _parse_arguments(self) -> None: self._arguments = parsed_arguments def _validate_arguments(self) -> None: - """ - This method validates all query parameters. + """This method validates all query parameters. :raises InvalidQueryParameters if there are invalid parameters. @@ -119,12 +113,7 @@ def _validate_arguments(self) -> None: elif isinstance(filter_value, list): all_types = [type(x) for x in filter_value] # All types must be {str, int, float} - assert all( - [ - isinstance(x, (str, int, float, bool)) - for x in filter_value - ] - ) + assert all([isinstance(x, (str, int, float, bool)) for x in filter_value]) # With this condition it is assured that all types are equal assert all_types.count(all_types[0]) == len(all_types) # All filter values must match the regex_sanitize_pattern to avoid injections @@ -166,8 +155,7 @@ def _validate_arguments(self) -> None: raise exc.InvalidQueryParameters() def filter(self, expression: BinaryExpression) -> "QueryFromRequestParameters": - """ - This method applies a sqlalchemy binary expression to the base query + """This method applies a sqlalchemy binary expression to the base query :param expression: Is the binary expression which is to be applied to the base query. @@ -178,50 +166,36 @@ def filter(self, expression: BinaryExpression) -> "QueryFromRequestParameters": @property def pagination(self) -> Optional[dict]: - """ - :return: The pagination parameters if they exist - """ + """:return: The pagination parameters if they exist""" return self._arguments.get("pagination", None) @property def filters(self) -> Optional[dict]: - """ - :return: The filter parameters if they exist - """ + """:return: The filter parameters if they exist""" return self._arguments.get("filter", None) @property def sorting(self) -> Optional[dict]: - """ - :return: The sorting parameters if they exist - """ + """:return: The sorting parameters if they exist""" return self._arguments.get("sort", None) def result(self) -> Tuple: - """ - Applies all filters to the query and returns the result + """Applies all filters to the query and returns the result :return: The queried data and the content-range header entry. """ - # Apply all filters if self.filters is not None: for filter_field, filter_value in self.filters.items(): # Case 1: One filter value and string. We use the contains method for this if isinstance(filter_value, str): - self._query = self._query.filter( - self._column_mapper[filter_field].contains(filter_value) - ) + self._query = self._query.filter(self._column_mapper[filter_field].contains(filter_value)) # Case 2: One filter value and int/float. We use the is_ method for this elif isinstance(filter_value, (int, float, bool)): - self._query = self._query.filter( - self._column_mapper[filter_field].is_(filter_value) - ) + self._query = self._query.filter(self._column_mapper[filter_field].is_(filter_value)) # Case 3: Multiple filter values else: - self._query = self._query.filter( - self._column_mapper[filter_field].in_(tuple(filter_value)) - ) + self._query = self._query.filter(self._column_mapper[filter_field].in_(tuple(filter_value))) # Apply sorting if self.sorting is not None: @@ -242,9 +216,7 @@ def result(self) -> Tuple: if self.pagination is not None: page = self.pagination.get("page") per_page = self.pagination.get("perPage") - self._query = self._query.paginate( - page=page, per_page=per_page, error_out=False - ) + self._query = self._query.paginate(page=page, per_page=per_page, error_out=False) data = self._query.items range_start = (page - 1) * per_page range_end = page * per_page - 1 @@ -261,8 +233,6 @@ def result(self) -> Tuple: # The value must be the total number of resources in the collection. # This allows the client interface to know how many pages of resources there are in total, # and build the pagination controls. - content_range = ( - f"{self._model.__table__.name}: {range_start}-{range_end}/{total_items}" - ) + content_range = f"{self._model.__table__.name}: {range_start}-{range_end}/{total_items}" return data, content_range diff --git a/shopdb/helpers/replenishments.py b/src/shop_db2/helpers/replenishments.py similarity index 67% rename from shopdb/helpers/replenishments.py rename to src/shop_db2/helpers/replenishments.py index 780622f..4f1659e 100644 --- a/shopdb/helpers/replenishments.py +++ b/src/shop_db2/helpers/replenishments.py @@ -6,17 +6,12 @@ from sqlalchemy.sql import func -from shopdb.api import db -from shopdb.models import Replenishment, ReplenishmentCollection +from shop_db2.api import db +from shop_db2.models import Replenishment, ReplenishmentCollection -def get_replenishment_amount_in_interval( - product_id: int, start: datetime.datetime, end: datetime.datetime -) -> int: - """ - Returns the sum of the amount of all replenishments of the given product. - """ - +def get_replenishment_amount_in_interval(product_id: int, start: datetime.datetime, end: datetime.datetime) -> int: + """Returns the sum of the amount of all replenishments of the given product.""" result = ( db.session.query(func.sum(Replenishment.amount)) .join( diff --git a/shopdb/helpers/stocktakings.py b/src/shop_db2/helpers/stocktakings.py similarity index 84% rename from shopdb/helpers/stocktakings.py rename to src/shop_db2/helpers/stocktakings.py index 03b981c..efcb698 100644 --- a/shopdb/helpers/stocktakings.py +++ b/src/shop_db2/helpers/stocktakings.py @@ -4,11 +4,16 @@ from sqlalchemy import and_, func -from shopdb.api import db -from shopdb.helpers.products import _get_product_mean_price_in_time_range -from shopdb.models import (Product, Purchase, Replenishment, - ReplenishmentCollection, Stocktaking, - StocktakingCollection) +from shop_db2.api import db +from shop_db2.helpers.products import _get_product_mean_price_in_time_range +from shop_db2.models import ( + Product, + Purchase, + Replenishment, + ReplenishmentCollection, + Stocktaking, + StocktakingCollection, +) def _get_balance_between_stocktakings(start, end): @@ -65,9 +70,7 @@ def _get_balance_between_stocktakings(start, end): # Determine how often the product was refilled in the time span and # how much was spent on it. res = ( - db.session.query( - func.sum(Replenishment.amount), func.sum(Replenishment.total_price) - ) + db.session.query(func.sum(Replenishment.amount), func.sum(Replenishment.total_price)) .join( ReplenishmentCollection, ReplenishmentCollection.id == Replenishment.replcoll_id, @@ -90,9 +93,7 @@ def _get_balance_between_stocktakings(start, end): replenish_sum_price = res[1] or 0 # Get the mean product price. - mean_price = _get_product_mean_price_in_time_range( - _id, start.timestamp, end.timestamp - ) + mean_price = _get_product_mean_price_in_time_range(_id, start.timestamp, end.timestamp) # Determine the number of products not purchased. difference = -(s - purchase_count + replenish_count - e) @@ -125,9 +126,7 @@ def _get_balance_between_stocktakings(start, end): def get_latest_non_revoked_stocktakingcollection() -> StocktakingCollection: - """ - This helper function returns the latest, non revoked stocktakingcollection. - """ + """This helper function returns the latest, non revoked stocktakingcollection.""" return ( db.session.query(StocktakingCollection) .order_by(StocktakingCollection.id.desc()) @@ -137,16 +136,11 @@ def get_latest_non_revoked_stocktakingcollection() -> StocktakingCollection: def get_latest_stocktaking_of_product(product_id: int) -> Optional[Stocktaking]: - """ - This helper function returns the latest stocktaking of a product. - """ - + """This helper function returns the latest stocktaking of a product.""" # Get the stocktaking count of the product result = ( db.session.query(Stocktaking) - .join( - StocktakingCollection, StocktakingCollection.id == Stocktaking.collection_id - ) + .join(StocktakingCollection, StocktakingCollection.id == Stocktaking.collection_id) .filter(Stocktaking.product_id == product_id) .filter(StocktakingCollection.revoked.is_(False)) .order_by(Stocktaking.id.desc()) diff --git a/shopdb/helpers/updater.py b/src/shop_db2/helpers/updater.py similarity index 87% rename from shopdb/helpers/updater.py rename to src/shop_db2/helpers/updater.py index 9c278a8..69887c3 100644 --- a/shopdb/helpers/updater.py +++ b/src/shop_db2/helpers/updater.py @@ -8,15 +8,14 @@ from flask import jsonify from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.helpers.validators import check_fields_and_types, check_forbidden -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.helpers.validators import check_fields_and_types, check_forbidden +from shop_db2.models import User def generic_update(model: db.Model, entry_id: int, data: dict, admin: Optional[User]): - """ - This is a generic function which handles all entry updates. "Normal" updates (like name, ...), which do not + """This is a generic function which handles all entry updates. "Normal" updates (like name, ...), which do not require any special treatment (like inserting other entries, ...) are handled with a simple "setattr(...)" operation. All other fields are updated by calling the set_FIELDNAME method, which must be implemented in the model class itself. @@ -45,9 +44,7 @@ def generic_update(model: db.Model, entry_id: int, data: dict, admin: Optional[U # Get the updateable fields. This only works with supported models. if not hasattr(item, "__updateable_fields__"): - raise Exception( - "The generic_update() function can only used with supported models" - ) + raise Exception("The generic_update() function can only used with supported models") # Get a dictionary containing all allowed fields and types updateable_fields: dict = item.__updateable_fields__ @@ -79,9 +76,7 @@ def generic_update(model: db.Model, entry_id: int, data: dict, admin: Optional[U if "admin_id" in signature(method).parameters.keys(): # If the admin_id parameter has a default value (e.g. def set_foo(admin_id=None, ...)), admin # privileges are not required to update the field - admin_required = isinstance( - signature(method).parameters["admin_id"].default, Signature.empty - ) + admin_required = isinstance(signature(method).parameters["admin_id"].default, Signature.empty) if admin_required and admin is None: raise exc.UnauthorizedAccess() diff --git a/shopdb/helpers/uploads.py b/src/shop_db2/helpers/uploads.py similarity index 86% rename from shopdb/helpers/uploads.py rename to src/shop_db2/helpers/uploads.py index a009478..7b36f14 100644 --- a/shopdb/helpers/uploads.py +++ b/src/shop_db2/helpers/uploads.py @@ -11,8 +11,9 @@ from PIL import Image -import configuration as config -import shopdb.exceptions as exc +import shop_db2.exceptions as exc + +import configuration as config # isort: skip def insert_image(file: dict) -> str: @@ -27,7 +28,7 @@ def insert_image(file: dict) -> str: # Check if the filename is valid filename = file["filename"].split(".")[0] - if filename is "" or not filename: + if filename in ["", None]: raise exc.InvalidFilename() # Check the file extension @@ -47,9 +48,7 @@ def insert_image(file: dict) -> str: # Check if the image is a valid image file. try: # Save the image to a temporary file. - temporary_file = tempfile.NamedTemporaryFile( - delete=False, suffix=f".{extension}" - ) + temporary_file = tempfile.NamedTemporaryFile(delete=False, suffix=f".{extension}") base64_data = file["value"].replace("data:image/png;base64,", "") temporary_file.write(base64.b64decode(base64_data)) temporary_file.close() @@ -70,8 +69,6 @@ def insert_image(file: dict) -> str: # Move the temporary image to its destination path. destination_filename = os.path.basename(temporary_file.name) - destination_path = os.path.join( - config.BaseConfig.UPLOAD_FOLDER, destination_filename - ) + destination_path = os.path.join(config.BaseConfig.UPLOAD_FOLDER, destination_filename) shutil.move(temporary_file.name, destination_path) return destination_filename diff --git a/shopdb/helpers/users.py b/src/shop_db2/helpers/users.py similarity index 87% rename from shopdb/helpers/users.py rename to src/shop_db2/helpers/users.py index eb3f696..20b814c 100644 --- a/shopdb/helpers/users.py +++ b/src/shop_db2/helpers/users.py @@ -4,15 +4,14 @@ from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, bcrypt, db -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import app, bcrypt, db +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import User def insert_user(data): - """ - This help function creates a new user with the given data. + """This help function creates a new user with the given data. :param data: Is the dictionary containing the data for the new user. @@ -24,7 +23,6 @@ def insert_user(data): :raises PasswordsDoNotMatch: If the passwords do not match. :raises CouldNotCreateEntry: If the new user cannot be created. """ - required = {"lastname": str} optional = {"firstname": str, "password": str, "password_repeat": str} diff --git a/shopdb/helpers/utils.py b/src/shop_db2/helpers/utils.py similarity index 89% rename from shopdb/helpers/utils.py rename to src/shop_db2/helpers/utils.py index ec4ab88..61292e0 100644 --- a/shopdb/helpers/utils.py +++ b/src/shop_db2/helpers/utils.py @@ -7,12 +7,11 @@ import dateutil.parser from flask import request -import shopdb.exceptions as exc +import shop_db2.exceptions as exc def json_body(): - """ - Returns the json data from the current request. + """Returns the json data from the current request. :return: The json body from the current request. @@ -25,8 +24,7 @@ def json_body(): def convert_minimal(data, fields): - """ - This function returns only the required attributes of all objects in + """This function returns only the required attributes of all objects in given list. :param data: The object from which the attributes are obtained. @@ -34,7 +32,6 @@ def convert_minimal(data, fields): :return: A dictionary with all requested attributes. """ - if not isinstance(data, list): data = [data] @@ -53,8 +50,7 @@ def convert_minimal(data, fields): def parse_timestamp(data: dict, required: bool) -> dict: - """ - Parses a timestamp in a input dictionary. If there is no timestamp and it's not required, nothing happens. + """Parses a timestamp in a input dictionary. If there is no timestamp and it's not required, nothing happens. Otherwise an exception gets raised. If a timestamp exists, it gets parsed. :param data: The input dictionary diff --git a/shopdb/helpers/validators.py b/src/shop_db2/helpers/validators.py similarity index 91% rename from shopdb/helpers/validators.py rename to src/shop_db2/helpers/validators.py index d64cf91..75a1493 100644 --- a/shopdb/helpers/validators.py +++ b/src/shop_db2/helpers/validators.py @@ -4,12 +4,11 @@ from flask import request -import shopdb.exceptions as exc +import shop_db2.exceptions as exc def check_forbidden(data, allowed_fields, row): - """ - This function checks whether any illegal fields exist in the data sent to + """This function checks whether any illegal fields exist in the data sent to the API with the request. If so, an exception is raised and the request is canceled. @@ -28,8 +27,7 @@ def check_forbidden(data, allowed_fields, row): def check_fields_and_types(data, required, optional=None): - """ - This function checks the given data for its types and existence. + """This function checks the given data for its types and existence. Required fields must exist, optional fields must not. :param data: The data sent to the API. @@ -43,7 +41,6 @@ def check_fields_and_types(data, required, optional=None): :raises DataIsMissing: If a required field is not in the data. :raises WrongType: If a field is of the wrong type. """ - if required and optional: allowed = dict(**required, **optional) elif required: @@ -66,8 +63,7 @@ def check_fields_and_types(data, required, optional=None): def check_allowed_parameters(allowed): - """ - This method checks all GET parameters for their type. + """This method checks all GET parameters for their type. :param allowed: A dictionary containing all allowed parameters and types. diff --git a/shopdb/models/__init__.py b/src/shop_db2/models/__init__.py similarity index 66% rename from shopdb/models/__init__.py rename to src/shop_db2/models/__init__.py index fe7e47a..10482b7 100644 --- a/shopdb/models/__init__.py +++ b/src/shop_db2/models/__init__.py @@ -10,11 +10,9 @@ from .purchase import Purchase, PurchaseRevoke from .rank import Rank from .rank_update import RankUpdate -from .replenishment import (Replenishment, ReplenishmentCollection, - ReplenishmentCollectionRevoke, ReplenishmentRevoke) +from .replenishment import Replenishment, ReplenishmentCollection, ReplenishmentCollectionRevoke, ReplenishmentRevoke from .revoke import Revoke -from .stocktaking import (Stocktaking, StocktakingCollection, - StocktakingCollectionRevoke) +from .stocktaking import Stocktaking, StocktakingCollection, StocktakingCollectionRevoke from .tag import Tag from .upload import Upload from .user import User diff --git a/shopdb/models/admin_update.py b/src/shop_db2/models/admin_update.py similarity index 92% rename from shopdb/models/admin_update.py rename to src/shop_db2/models/admin_update.py index 7bd6550..7b5d50d 100644 --- a/shopdb/models/admin_update.py +++ b/src/shop_db2/models/admin_update.py @@ -5,8 +5,8 @@ from sqlalchemy import func from sqlalchemy.orm import validates -from shopdb.exceptions import UnauthorizedAccess -from shopdb.shared import db +from shop_db2.exceptions import UnauthorizedAccess +from shop_db2.shared import db class AdminUpdate(db.Model): diff --git a/shopdb/models/deposit.py b/src/shop_db2/models/deposit.py similarity index 98% rename from shopdb/models/deposit.py rename to src/shop_db2/models/deposit.py index 3b155dc..d8298f2 100644 --- a/shopdb/models/deposit.py +++ b/src/shop_db2/models/deposit.py @@ -5,7 +5,7 @@ from sqlalchemy import func from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -from shopdb.shared import db +from shop_db2.shared import db from .revoke import Revoke diff --git a/shopdb/models/product.py b/src/shop_db2/models/product.py similarity index 92% rename from shopdb/models/product.py rename to src/shop_db2/models/product.py index 0cc9b7a..5449fea 100644 --- a/shopdb/models/product.py +++ b/src/shop_db2/models/product.py @@ -9,11 +9,17 @@ from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import column_property, validates -from shopdb.exceptions import (CouldNotCreateEntry, EntryAlreadyExists, - EntryNotFound, InvalidData, NoRemainingTag, - NothingHasChanged, UnauthorizedAccess) -from shopdb.helpers.uploads import insert_image -from shopdb.shared import db +from shop_db2.exceptions import ( + CouldNotCreateEntry, + EntryAlreadyExists, + EntryNotFound, + InvalidData, + NoRemainingTag, + NothingHasChanged, + UnauthorizedAccess, +) +from shop_db2.helpers.uploads import insert_image +from shop_db2.shared import db class Product(db.Model): @@ -88,9 +94,7 @@ def validate_admin(self, key, created_by): @property def is_for_sale(self): - """ - Returns whether this product is for sale for unprivileged users - """ + """Returns whether this product is for sale for unprivileged users""" return all(map(lambda tag: tag.is_for_sale, self.tags)) @hybrid_property @@ -138,9 +142,7 @@ def get_pricehistory(self, start_date=None, end_date=None): ) # Map the result to a dictionary containing all price changes. - return list( - map(lambda p: {"id": p.id, "timestamp": p.timestamp, "price": p.price}, res) - ) + return list(map(lambda p: {"id": p.id, "timestamp": p.timestamp, "price": p.price}, res)) @hybrid_method def set_price(self, price, admin_id): diff --git a/shopdb/models/product_price.py b/src/shop_db2/models/product_price.py similarity index 90% rename from shopdb/models/product_price.py rename to src/shop_db2/models/product_price.py index 08523c4..92e4035 100644 --- a/shopdb/models/product_price.py +++ b/src/shop_db2/models/product_price.py @@ -5,8 +5,8 @@ from sqlalchemy import func from sqlalchemy.orm import validates -from shopdb.exceptions import UnauthorizedAccess -from shopdb.shared import db +from shop_db2.exceptions import UnauthorizedAccess +from shop_db2.shared import db class ProductPrice(db.Model): diff --git a/shopdb/models/product_tag_assignment.py b/src/shop_db2/models/product_tag_assignment.py similarity index 89% rename from shopdb/models/product_tag_assignment.py rename to src/shop_db2/models/product_tag_assignment.py index f03eade..82582f8 100644 --- a/shopdb/models/product_tag_assignment.py +++ b/src/shop_db2/models/product_tag_assignment.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -from shopdb.shared import db +from shop_db2.shared import db product_tag_assignments = db.Table( "product_tag_assignments", diff --git a/shopdb/models/purchase.py b/src/shop_db2/models/purchase.py similarity index 96% rename from shopdb/models/purchase.py rename to src/shop_db2/models/purchase.py index ac8a578..c1b7ba1 100644 --- a/shopdb/models/purchase.py +++ b/src/shop_db2/models/purchase.py @@ -6,8 +6,8 @@ from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import validates -from shopdb.exceptions import EntryNotRevocable, UserIsNotVerified -from shopdb.shared import db +from shop_db2.exceptions import EntryNotRevocable, UserIsNotVerified +from shop_db2.shared import db class PurchaseRevoke(db.Model): diff --git a/shopdb/models/rank.py b/src/shop_db2/models/rank.py similarity index 94% rename from shopdb/models/rank.py rename to src/shop_db2/models/rank.py index 3727fa3..c3eaf56 100644 --- a/shopdb/models/rank.py +++ b/src/shop_db2/models/rank.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -from shopdb.shared import db +from shop_db2.shared import db class Rank(db.Model): diff --git a/shopdb/models/rank_update.py b/src/shop_db2/models/rank_update.py similarity index 91% rename from shopdb/models/rank_update.py rename to src/shop_db2/models/rank_update.py index db6218e..cb01943 100644 --- a/shopdb/models/rank_update.py +++ b/src/shop_db2/models/rank_update.py @@ -5,8 +5,8 @@ from sqlalchemy import func from sqlalchemy.orm import validates -from shopdb.exceptions import UnauthorizedAccess -from shopdb.shared import db +from shop_db2.exceptions import UnauthorizedAccess +from shop_db2.shared import db class RankUpdate(db.Model): diff --git a/shopdb/models/replenishment.py b/src/shop_db2/models/replenishment.py similarity index 79% rename from shopdb/models/replenishment.py rename to src/shop_db2/models/replenishment.py index d262bdb..4ecaf0a 100644 --- a/shopdb/models/replenishment.py +++ b/src/shop_db2/models/replenishment.py @@ -6,9 +6,9 @@ from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import column_property -from shopdb.exceptions import EntryNotRevocable -from shopdb.helpers.utils import parse_timestamp -from shopdb.shared import db +from shop_db2.exceptions import EntryNotRevocable +from shop_db2.helpers.utils import parse_timestamp +from shop_db2.shared import db from .revoke import Revoke @@ -24,9 +24,7 @@ class Replenishment(db.Model): __updateable_fields__ = {"revoked": bool, "amount": int, "total_price": int} id = db.Column(db.Integer, primary_key=True) - replcoll_id = db.Column( - db.Integer, db.ForeignKey("replenishmentcollections.id"), nullable=False - ) + replcoll_id = db.Column(db.Integer, db.ForeignKey("replenishmentcollections.id"), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey("products.id"), nullable=False) revoked = db.Column(db.Boolean, nullable=False, default=False) amount = db.Column(db.Integer, nullable=False) @@ -43,9 +41,7 @@ class Replenishment(db.Model): def set_revoked(self, revoked, admin_id): # Get all not revoked replenishments corresponding to the # replenishmentcollection before changes are made - non_revoked_replenishments = ( - self.replenishmentcollection.replenishments.filter_by(revoked=False).all() - ) + non_revoked_replenishments = self.replenishmentcollection.replenishments.filter_by(revoked=False).all() if not revoked and not non_revoked_replenishments: dr = ReplenishmentCollectionRevoke( revoked=False, @@ -60,9 +56,7 @@ def set_revoked(self, revoked, admin_id): db.session.add(dr) # Check if ReplenishmentCollection still has non-revoked replenishments - non_revoked_replenishments = ( - self.replenishmentcollection.replenishments.filter_by(revoked=False).all() - ) + non_revoked_replenishments = self.replenishmentcollection.replenishments.filter_by(revoked=False).all() if not self.replenishmentcollection.revoked and not non_revoked_replenishments: dr = ReplenishmentCollectionRevoke( revoked=True, @@ -74,9 +68,7 @@ def set_revoked(self, revoked, admin_id): @hybrid_property def revokehistory(self): - res = ReplenishmentRevoke.query.filter( - ReplenishmentRevoke.repl_id == self.id - ).all() + res = ReplenishmentRevoke.query.filter(ReplenishmentRevoke.repl_id == self.id).all() revokehistory = [] for revoke in res: revokehistory.append( @@ -92,9 +84,7 @@ def revokehistory(self): class ReplenishmentCollectionRevoke(Revoke, db.Model): __tablename__ = "replenishmentcollectionrevoke" - replcoll_id = db.Column( - db.Integer, db.ForeignKey("replenishmentcollections.id"), nullable=False - ) + replcoll_id = db.Column(db.Integer, db.ForeignKey("replenishmentcollections.id"), nullable=False) class ReplenishmentCollection(db.Model): @@ -107,9 +97,7 @@ class ReplenishmentCollection(db.Model): seller_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) revoked = db.Column(db.Boolean, nullable=False, default=False) comment = db.Column(db.String(64), nullable=False) - replenishments = db.relationship( - "Replenishment", lazy="dynamic", foreign_keys="Replenishment.replcoll_id" - ) + replenishments = db.relationship("Replenishment", lazy="dynamic", foreign_keys="Replenishment.replcoll_id") price = column_property( select([func.coalesce(func.sum(Replenishment.total_price), 0)]) @@ -125,9 +113,7 @@ def set_revoked(self, revoked, admin_id): if not revoked and not non_revoked_replenishments: raise EntryNotRevocable() - dr = ReplenishmentCollectionRevoke( - revoked=revoked, admin_id=admin_id, replcoll_id=self.id - ) + dr = ReplenishmentCollectionRevoke(revoked=revoked, admin_id=admin_id, replcoll_id=self.id) self.revoked = revoked db.session.add(dr) @@ -138,9 +124,7 @@ def set_timestamp(self, timestamp: str): @hybrid_property def revokehistory(self): - res = ReplenishmentCollectionRevoke.query.filter( - ReplenishmentCollectionRevoke.replcoll_id == self.id - ).all() + res = ReplenishmentCollectionRevoke.query.filter(ReplenishmentCollectionRevoke.replcoll_id == self.id).all() revokehistory = [] for revoke in res: revokehistory.append( diff --git a/shopdb/models/revoke.py b/src/shop_db2/models/revoke.py similarity index 85% rename from shopdb/models/revoke.py rename to src/shop_db2/models/revoke.py index ea14673..22c179a 100644 --- a/shopdb/models/revoke.py +++ b/src/shop_db2/models/revoke.py @@ -6,13 +6,12 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import validates -from shopdb.exceptions import UnauthorizedAccess -from shopdb.shared import db +from shop_db2.exceptions import UnauthorizedAccess +from shop_db2.shared import db class Revoke: - """ - All revokes that must be executed by an administrator (Deposit, + """All revokes that must be executed by an administrator (Deposit, Replenishment, ...) had code duplications. For this reason, there is now a class from which all these revokes can inherit to save code. """ diff --git a/shopdb/models/stocktaking.py b/src/shop_db2/models/stocktaking.py similarity index 72% rename from shopdb/models/stocktaking.py rename to src/shop_db2/models/stocktaking.py index fa1a282..c47100a 100644 --- a/shopdb/models/stocktaking.py +++ b/src/shop_db2/models/stocktaking.py @@ -5,8 +5,8 @@ from sqlalchemy import func from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property -from shopdb.exceptions import InvalidAmount -from shopdb.shared import db +from shop_db2.exceptions import InvalidAmount +from shop_db2.shared import db from .revoke import Revoke @@ -18,9 +18,7 @@ class Stocktaking(db.Model): id = db.Column(db.Integer, primary_key=True) count = db.Column(db.Integer, nullable=False) product_id = db.Column(db.Integer, db.ForeignKey("products.id"), nullable=False) - collection_id = db.Column( - db.Integer, db.ForeignKey("stocktakingcollections.id"), nullable=False - ) + collection_id = db.Column(db.Integer, db.ForeignKey("stocktakingcollections.id"), nullable=False) @hybrid_method def set_count(self, count): @@ -44,23 +42,17 @@ class StocktakingCollection(db.Model): timestamp = db.Column(db.DateTime, default=func.now(), nullable=False) revoked = db.Column(db.Boolean, nullable=False, default=False) admin_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) - stocktakings = db.relationship( - "Stocktaking", lazy="dynamic", foreign_keys="Stocktaking.collection_id" - ) + stocktakings = db.relationship("Stocktaking", lazy="dynamic", foreign_keys="Stocktaking.collection_id") @hybrid_method def set_revoked(self, revoked, admin_id): - sr = StocktakingCollectionRevoke( - revoked=revoked, admin_id=admin_id, collection_id=self.id - ) + sr = StocktakingCollectionRevoke(revoked=revoked, admin_id=admin_id, collection_id=self.id) self.revoked = revoked db.session.add(sr) @hybrid_property def revokehistory(self): - res = StocktakingCollectionRevoke.query.filter( - StocktakingCollectionRevoke.collection_id == self.id - ).all() + res = StocktakingCollectionRevoke.query.filter(StocktakingCollectionRevoke.collection_id == self.id).all() revokehistory = [] for revoke in res: revokehistory.append( @@ -76,6 +68,4 @@ def revokehistory(self): class StocktakingCollectionRevoke(Revoke, db.Model): __tablename__ = "stocktakingcollectionrevokes" - collection_id = db.Column( - db.Integer, db.ForeignKey("stocktakingcollections.id"), nullable=False - ) + collection_id = db.Column(db.Integer, db.ForeignKey("stocktakingcollections.id"), nullable=False) diff --git a/shopdb/models/tag.py b/src/shop_db2/models/tag.py similarity index 94% rename from shopdb/models/tag.py rename to src/shop_db2/models/tag.py index f4536d5..cda405a 100644 --- a/shopdb/models/tag.py +++ b/src/shop_db2/models/tag.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -from shopdb.shared import db +from shop_db2.shared import db class Tag(db.Model): diff --git a/shopdb/models/upload.py b/src/shop_db2/models/upload.py similarity index 93% rename from shopdb/models/upload.py rename to src/shop_db2/models/upload.py index 96df6bc..e8d2a9e 100644 --- a/shopdb/models/upload.py +++ b/src/shop_db2/models/upload.py @@ -4,7 +4,7 @@ from sqlalchemy import func -from shopdb.shared import db +from shop_db2.shared import db class Upload(db.Model): diff --git a/shopdb/models/user.py b/src/shop_db2/models/user.py similarity index 82% rename from shopdb/models/user.py rename to src/shop_db2/models/user.py index 55439b7..1e35c58 100644 --- a/shopdb/models/user.py +++ b/src/shop_db2/models/user.py @@ -7,11 +7,15 @@ from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.orm import column_property -from shopdb.exceptions import (CouldNotCreateEntry, NoRemainingAdmin, - NothingHasChanged, UserAlreadyVerified, - UserNeedsPassword) -from shopdb.helpers.uploads import insert_image -from shopdb.shared import db +from shop_db2.exceptions import ( + CouldNotCreateEntry, + NoRemainingAdmin, + NothingHasChanged, + UserAlreadyVerified, + UserNeedsPassword, +) +from shop_db2.helpers.uploads import insert_image +from shop_db2.shared import db class User(db.Model): @@ -63,10 +67,7 @@ class User(db.Model): # Column property for the verification_date verification_date = column_property( - select([UserVerification.timestamp]) - .where(UserVerification.user_id == id) - .limit(1) - .as_scalar() + select([UserVerification.timestamp]).where(UserVerification.user_id == id).limit(1).as_scalar() ) # Column property for the rank_id @@ -107,19 +108,13 @@ class User(db.Model): # A users credit is the sum of all amounts that increase his credit (Deposits, ReplenishmentCollections) # and all amounts that decrease it (Purchases) credit = column_property( - _replenishmentcollection_sum.expression - + _deposit_sum.expression - - _purchase_sum.expression + _replenishmentcollection_sum.expression + _deposit_sum.expression - _purchase_sum.expression ) # Link to all purchases of a user. - purchases = db.relationship( - "Purchase", lazy="dynamic", foreign_keys="Purchase.user_id" - ) + purchases = db.relationship("Purchase", lazy="dynamic", foreign_keys="Purchase.user_id") # Link to all deposits of a user. - deposits = db.relationship( - "Deposit", lazy="dynamic", foreign_keys="Deposit.user_id" - ) + deposits = db.relationship("Deposit", lazy="dynamic", foreign_keys="Deposit.user_id") # Link to all deposits of a user. replenishmentcollections = db.relationship( "ReplenishmentCollection", @@ -157,11 +152,7 @@ def set_imagename(self, image, admin_id): def is_admin(self): from .admin_update import AdminUpdate - au = ( - AdminUpdate.query.filter_by(user_id=self.id) - .order_by(AdminUpdate.id.desc()) - .first() - ) + au = AdminUpdate.query.filter_by(user_id=self.id).order_by(AdminUpdate.id.desc()).first() if au is None: return False return au.is_admin @@ -219,10 +210,10 @@ def rank(self): @hybrid_property def favorites(self): - """ - Returns the product ids of the user's favorite products in + """Returns the product ids of the user's favorite products in descending order of number. Inactive products those who are not for sale are ignored. + Args: self: self Returns: @@ -234,9 +225,7 @@ def favorites(self): from .tag import Tag # Get a list of all invalid tag ids (as SQL subquery) - invalid_tag_ids = ( - db.session.query(Tag.id).filter(Tag.is_for_sale.is_(False)).subquery() - ) + invalid_tag_ids = db.session.query(Tag.id).filter(Tag.is_for_sale.is_(False)).subquery() # Get a list of all products to which this tag is assigned invalid_product_ids = ( db.session.query(product_tag_assignments.c.product_id) @@ -244,24 +233,16 @@ def favorites(self): .subquery() ) # Get a list of all inactive product ids - inactive_product_ids = ( - db.session.query(Product.id).filter(Product.active.is_(False)).subquery() - ) + inactive_product_ids = db.session.query(Product.id).filter(Product.active.is_(False)).subquery() result = ( db.session.query(Purchase.product_id) .filter(Purchase.user_id == self.id) # Get only user purchases .group_by(Purchase.product_id) # Group by products - .filter( - Purchase.product_id.notin_(invalid_product_ids) - ) # Get only products which are for sale - .filter( - Purchase.product_id.notin_(inactive_product_ids) - ) # Get only products which are active + .filter(Purchase.product_id.notin_(invalid_product_ids)) # Get only products which are for sale + .filter(Purchase.product_id.notin_(inactive_product_ids)) # Get only products which are active .filter(Purchase.revoked.is_(False)) # Get only non revoked purchases - .order_by( - func.sum(Purchase.amount).desc() - ) # Order by the sum of purchase amount + .order_by(func.sum(Purchase.amount).desc()) # Order by the sum of purchase amount .all() ) return [item.product_id for item in result] diff --git a/shopdb/models/user_verification.py b/src/shop_db2/models/user_verification.py similarity index 78% rename from shopdb/models/user_verification.py rename to src/shop_db2/models/user_verification.py index 45ca0ff..e07e8e7 100644 --- a/shopdb/models/user_verification.py +++ b/src/shop_db2/models/user_verification.py @@ -5,8 +5,8 @@ from sqlalchemy import func from sqlalchemy.orm import validates -from shopdb.exceptions import UnauthorizedAccess -from shopdb.shared import db +from shop_db2.exceptions import UnauthorizedAccess +from shop_db2.shared import db class UserVerification(db.Model): @@ -15,9 +15,7 @@ class UserVerification(db.Model): id = db.Column(db.Integer, primary_key=True) timestamp = db.Column(db.DateTime, default=func.now(), nullable=False) admin_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) - user_id = db.Column( - db.Integer, db.ForeignKey("users.id"), nullable=False, unique=True - ) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, unique=True) @validates("admin_id") def validate_admin(self, key, admin_id): diff --git a/shopdb/helpers/__init__.py b/src/shop_db2/py.typed similarity index 100% rename from shopdb/helpers/__init__.py rename to src/shop_db2/py.typed diff --git a/shopdb/routes/__init__.py b/src/shop_db2/routes/__init__.py similarity index 100% rename from shopdb/routes/__init__.py rename to src/shop_db2/routes/__init__.py diff --git a/shopdb/routes/backups.py b/src/shop_db2/routes/backups.py similarity index 91% rename from shopdb/routes/backups.py rename to src/shop_db2/routes/backups.py index 8c54258..cb96552 100644 --- a/shopdb/routes/backups.py +++ b/src/shop_db2/routes/backups.py @@ -9,15 +9,14 @@ from flask import jsonify -from shopdb.api import app -from shopdb.helpers.decorators import adminRequired +from shop_db2.api import app +from shop_db2.helpers.decorators import adminRequired @app.route("/backups", methods=["GET"]) @adminRequired def list_backups(admin): - """ - Returns a dictionary with all backups in the backup folder. + """Returns a dictionary with all backups in the backup folder. The following backup directory structure is assumed for this function: [Year]/[Month]/[Day]/shop-db_[Year]-[Month]-[Day]_[Hour]-[Minute].dump diff --git a/shopdb/routes/deposits.py b/src/shop_db2/routes/deposits.py similarity index 86% rename from shopdb/routes/deposits.py rename to src/shop_db2/routes/deposits.py index c6a9978..60a01a2 100644 --- a/shopdb/routes/deposits.py +++ b/src/shop_db2/routes/deposits.py @@ -5,22 +5,21 @@ from flask import jsonify, request from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.deposits import insert_deposit -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Deposit +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.deposits import insert_deposit +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Deposit @app.route("/deposits", methods=["GET"]) @adminRequired def list_deposits(admin): - """ - Returns a list of all deposits. + """Returns a list of all deposits. :param admin: Is the administrator user, determined by @adminRequired. @@ -37,8 +36,7 @@ def list_deposits(admin): @app.route("/deposits", methods=["POST"]) @adminRequired def create_deposit(admin): - """ - Insert a new deposit. + """Insert a new deposit. :param admin: Is the administrator user, determined by @adminRequired. @@ -69,8 +67,7 @@ def create_deposit(admin): @app.route("/deposits/batch", methods=["POST"]) @adminRequired def create_batch_deposit(admin): - """ - Insert a new batch deposit. + """Insert a new batch deposit. :param admin: Is the administrator user, determined by @adminRequired. @@ -108,8 +105,7 @@ def create_batch_deposit(admin): @app.route("/deposits/", methods=["GET"]) def get_deposit(deposit_id): - """ - Returns the deposit with the requested id. + """Returns the deposit with the requested id. :param deposit_id: Is the deposit id. @@ -138,8 +134,7 @@ def get_deposit(deposit_id): @app.route("/deposits/", methods=["PUT"]) @adminRequired def update_deposit(admin, deposit_id): - """ - Update the deposit with the given id. + """Update the deposit with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param deposit_id: Is the deposit id. diff --git a/shopdb/routes/financial_overview.py b/src/shop_db2/routes/financial_overview.py similarity index 68% rename from shopdb/routes/financial_overview.py rename to src/shop_db2/routes/financial_overview.py index ba8ed0d..b05acfc 100644 --- a/shopdb/routes/financial_overview.py +++ b/src/shop_db2/routes/financial_overview.py @@ -4,18 +4,16 @@ from flask import jsonify -from shopdb.api import app -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.stocktakings import _get_balance_between_stocktakings -from shopdb.models import (Deposit, Purchase, ReplenishmentCollection, - StocktakingCollection) +from shop_db2.api import app +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.stocktakings import _get_balance_between_stocktakings +from shop_db2.models import Deposit, Purchase, ReplenishmentCollection, StocktakingCollection @app.route("/financial_overview", methods=["GET"]) @adminRequired def get_financial_overview(admin): - """ - The financial status of the entire project can be retrieved via this route. + """The financial status of the entire project can be retrieved via this route. All purchases, deposits and replenishmentcollections are used for this purpose. The items are cleared once to a number indicating whether the community has debt or surplus money. In addition, the @@ -26,7 +24,6 @@ def get_financial_overview(admin): :return: A dictionary with the individually calculated values. """ - # Query all purchases, purchases = Purchase.query.filter(Purchase.revoked.is_(False)).all() @@ -34,16 +31,12 @@ def get_financial_overview(admin): deposits = Deposit.query.filter(Deposit.revoked.is_(False)).all() # Query all replenishment collections. - replcolls = ReplenishmentCollection.query.filter( - ReplenishmentCollection.revoked.is_(False) - ).all() + replcolls = ReplenishmentCollection.query.filter(ReplenishmentCollection.revoked.is_(False)).all() # Get the balance between the first and the last stocktaking. # If there is no stocktaking or only one stocktaking, the balance is 0. stock_first = StocktakingCollection.query.order_by(StocktakingCollection.id).first() - stock_last = StocktakingCollection.query.order_by( - StocktakingCollection.id.desc() - ).first() + stock_last = StocktakingCollection.query.order_by(StocktakingCollection.id.desc()).first() if not all([stock_first, stock_last]) or stock_first is stock_last: pos_stock = 0 @@ -59,21 +52,11 @@ def get_financial_overview(admin): # - Replenishmentcollections with a negative price # - Profits between stocktakings - pos_pur = sum( - map( - abs, list(filter(lambda x: x >= 0, list(map(lambda x: x.price, purchases)))) - ) - ) + pos_pur = sum(map(abs, list(filter(lambda x: x >= 0, list(map(lambda x: x.price, purchases)))))) - pos_dep = sum( - map( - abs, list(filter(lambda x: x >= 0, list(map(lambda x: x.amount, deposits)))) - ) - ) + pos_dep = sum(map(abs, list(filter(lambda x: x >= 0, list(map(lambda x: x.amount, deposits)))))) - neg_rep = sum( - map(abs, list(filter(lambda x: x < 0, list(map(lambda x: x.price, replcolls))))) - ) + neg_rep = sum(map(abs, list(filter(lambda x: x < 0, list(map(lambda x: x.price, replcolls)))))) sum_incomes = sum([pos_pur, pos_dep, neg_rep, pos_stock]) @@ -93,19 +76,11 @@ def get_financial_overview(admin): # - Turnovers with a negative amount # - Replenishmentcollections with a positive price # - Losses between stocktakings - neg_pur = sum( - map(abs, list(filter(lambda x: x < 0, list(map(lambda x: x.price, purchases))))) - ) - - neg_dep = sum( - map(abs, list(filter(lambda x: x < 0, list(map(lambda x: x.amount, deposits))))) - ) - - pos_rep = sum( - map( - abs, list(filter(lambda x: x >= 0, list(map(lambda x: x.price, replcolls)))) - ) - ) + neg_pur = sum(map(abs, list(filter(lambda x: x < 0, list(map(lambda x: x.price, purchases)))))) + + neg_dep = sum(map(abs, list(filter(lambda x: x < 0, list(map(lambda x: x.amount, deposits)))))) + + pos_rep = sum(map(abs, list(filter(lambda x: x >= 0, list(map(lambda x: x.price, replcolls)))))) sum_expenses = sum([neg_pur, neg_dep, pos_rep, neg_stock]) diff --git a/shopdb/routes/images.py b/src/shop_db2/routes/images.py similarity index 84% rename from shopdb/routes/images.py rename to src/shop_db2/routes/images.py index 4cb91f9..99c23ce 100644 --- a/shopdb/routes/images.py +++ b/src/shop_db2/routes/images.py @@ -6,15 +6,14 @@ from flask import send_from_directory -import shopdb.exceptions as exc -from shopdb.api import app +import shop_db2.exceptions as exc +from shop_db2.api import app @app.route("/images", methods=["GET"], defaults={"imagename": None}) @app.route("/images/", methods=["GET"]) def get_image(imagename): - """ - A picture can be requested via this route. If the image is not found or if + """A picture can be requested via this route. If the image is not found or if the image name is empty, a default image will be returned. :param imagename: Is the name of the requested image. diff --git a/shopdb/routes/login.py b/src/shop_db2/routes/login.py similarity index 88% rename from shopdb/routes/login.py rename to src/shop_db2/routes/login.py index c0a8aec..a78ba3a 100644 --- a/shopdb/routes/login.py +++ b/src/shop_db2/routes/login.py @@ -7,17 +7,16 @@ import jwt from flask import jsonify -import shopdb.exceptions as exc -from shopdb.api import app, bcrypt -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import app, bcrypt +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import User @app.route("/login", methods=["POST"], endpoint="login") def login(): - """ - Registered users can log in on this route. + """Registered users can log in on this route. :return: A temporary valid token, which users can use to identify themselves when making requests to diff --git a/shopdb/routes/maintenance.py b/src/shop_db2/routes/maintenance.py similarity index 79% rename from shopdb/routes/maintenance.py rename to src/shop_db2/routes/maintenance.py index df76ee8..4924838 100644 --- a/shopdb/routes/maintenance.py +++ b/src/shop_db2/routes/maintenance.py @@ -7,30 +7,28 @@ from flask import jsonify -import shopdb.exceptions as exc -from configuration import PATH -from shopdb.api import app -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.utils import json_body -from shopdb.helpers.validators import check_fields_and_types +import shop_db2.exceptions as exc +from shop_db2.api import app +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.utils import json_body +from shop_db2.helpers.validators import check_fields_and_types + +from configuration import PATH # isort: skip @app.route("/maintenance", methods=["GET"]) def get_maintenance_mode(): - """ - This route returns whether the app is in maintenance mode. + """This route returns whether the app is in maintenance mode. :return: A message with the maintenance mode. """ - return jsonify(app.config["MAINTENANCE"]) @app.route("/maintenance", methods=["POST"], endpoint="maintenance") @adminRequired def set_maintenance_mode(admin): - """ - This route can be used by an administrator to switch the maintenance mode + """This route can be used by an administrator to switch the maintenance mode on or off. :param admin: Is the administrator user, determined by @@ -46,7 +44,6 @@ def set_maintenance_mode(admin): :return: A message with the new maintenance mode. """ - data = json_body() # Check all items in the json body. required = {"state": bool} @@ -66,9 +63,7 @@ def set_maintenance_mode(admin): RE_MAINENTANCE_PATTERN = r"(.*)(MAINTENANCE)([^\w]*)(True|False)" with open(os.path.join(PATH, "configuration.py"), "r") as config_file: config_file_content = config_file.read() - new_config_file = re.sub( - RE_MAINENTANCE_PATTERN, r"\1\2\3{}".format(str(new_state)), config_file_content - ) + new_config_file = re.sub(RE_MAINENTANCE_PATTERN, r"\1\2\3{}".format(str(new_state)), config_file_content) with open(os.path.join(PATH, "configuration.py"), "w") as config_file: config_file.write(new_config_file) diff --git a/shopdb/routes/products.py b/src/shop_db2/routes/products.py similarity index 90% rename from shopdb/routes/products.py rename to src/shop_db2/routes/products.py index 64dae27..3726365 100644 --- a/shopdb/routes/products.py +++ b/src/shop_db2/routes/products.py @@ -5,22 +5,21 @@ from flask import jsonify, request from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -import shopdb.helpers.products as product_helpers -from shopdb.api import app, db -from shopdb.helpers.decorators import adminOptional, adminRequired -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Product, Tag, product_tag_assignments +import shop_db2.exceptions as exc +import shop_db2.helpers.products as product_helpers +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminOptional, adminRequired +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Product, Tag, product_tag_assignments @app.route("/products", methods=["GET"]) @adminOptional def list_products(admin): - """ - Returns a list of all products. + """Returns a list of all products. :param admin: Is the administrator user, determined by @adminOptional. @@ -55,8 +54,7 @@ def list_products(admin): @app.route("/products", methods=["POST"]) @adminRequired def create_product(admin): - """ - Route to create a new product. + """Route to create a new product. :param admin: Is the administrator user, determined by @adminRequired. @@ -131,8 +129,7 @@ def create_product(admin): @app.route("/products/", methods=["GET"]) @adminOptional def get_product(admin, product_id): - """ - Returns the product with the requested id. + """Returns the product with the requested id. :param admin: Is the administrator user, determined by @adminOptional. @@ -186,8 +183,7 @@ def get_product(admin, product_id): @app.route("/products//stock", methods=["GET"]) def get_product_stock(product_id): - """ - Returns the theoretical stock level of a product. + """Returns the theoretical stock level of a product. The theoretical stock level of a product is the result of the number determined in the last stocktaking minus the number of purchases @@ -201,7 +197,6 @@ def get_product_stock(product_id): :raises EntryNotFound: If the product with this ID does not exist. """ - # Check, whether the requested product exists product = Product.query.filter(Product.id == product_id).first() if not product: @@ -220,8 +215,7 @@ def get_product_stock(product_id): @app.route("/products//pricehistory", methods=["GET"]) @adminRequired def get_product_pricehistory(admin, product_id): - """ - Returns the pricehistory of the product with the given id. If only want to + """Returns the pricehistory of the product with the given id. If only want to query a part of the history in a range there are optional request arguments: - start_date: Is the unix timestamp of the start date. - end_date: Is the unix timestamp of the end date. @@ -235,7 +229,6 @@ def get_product_pricehistory(admin, product_id): :return: The pricehistory of the product. """ - # Check, whether the product exists. product = Product.query.filter(Product.id == product_id).first() if not product: @@ -265,8 +258,7 @@ def get_product_pricehistory(admin, product_id): @app.route("/products/", methods=["PUT"]) @adminRequired def update_product(admin, product_id): - """ - Update the product with the given id. + """Update the product with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param product_id: Is the product id. diff --git a/shopdb/routes/purchases.py b/src/shop_db2/routes/purchases.py similarity index 86% rename from shopdb/routes/purchases.py rename to src/shop_db2/routes/purchases.py index 54b4589..1f35141 100644 --- a/shopdb/routes/purchases.py +++ b/src/shop_db2/routes/purchases.py @@ -6,21 +6,20 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.sql import exists -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.helpers.decorators import adminOptional -from shopdb.helpers.purchases import insert_purchase -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.models import Purchase, PurchaseRevoke +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminOptional +from shop_db2.helpers.purchases import insert_purchase +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.models import Purchase, PurchaseRevoke @app.route("/purchases", methods=["GET"]) @adminOptional def list_purchases(admin): - """ - Returns a list of all purchases. If this route is called by an + """Returns a list of all purchases. If this route is called by an administrator, all information is returned. However, if it is called without further rights, a minimal version is returned. @@ -56,8 +55,7 @@ def list_purchases(admin): @app.route("/purchases", methods=["POST"]) @adminOptional def create_purchase(admin): - """ - Insert a new purchase. + """Insert a new purchase. :param admin: Is the administrator user, determined by @adminOptional. @@ -94,8 +92,7 @@ def create_purchase(admin): @app.route("/purchases/", methods=["GET"]) def get_purchase(purchase_id): - """ - Returns the purchase with the requested id. + """Returns the purchase with the requested id. :param purchase_id: Is the purchase id. @@ -124,8 +121,7 @@ def get_purchase(purchase_id): @app.route("/purchases/", methods=["PUT"]) @adminOptional def update_purchase(admin, purchase_id): - """ - Update the purchase with the given id. + """Update the purchase with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param purchase_id: Is the purchase id. diff --git a/shopdb/routes/ranks.py b/src/shop_db2/routes/ranks.py similarity index 82% rename from shopdb/routes/ranks.py rename to src/shop_db2/routes/ranks.py index 22a1bcc..b5b47af 100644 --- a/shopdb/routes/ranks.py +++ b/src/shop_db2/routes/ranks.py @@ -5,20 +5,19 @@ from flask import jsonify, request from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Rank +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Rank @app.route("/ranks", methods=["GET"]) def list_ranks(): - """ - Returns a list of all ranks. + """Returns a list of all ranks. :return: A list of all ranks. """ @@ -33,8 +32,7 @@ def list_ranks(): @app.route("/ranks", methods=["POST"]) @adminRequired def create_rank(admin): - """ - Route to create a new rank. + """Route to create a new rank. :param admin: Is the administrator user, determined by @adminRequired. @@ -71,8 +69,7 @@ def create_rank(admin): @app.route("/ranks/", methods=["GET"]) def get_rank(rank_id): - """ - Returns the rank with the requested id. + """Returns the rank with the requested id. :param rank_id: Is the rank id. @@ -91,8 +88,7 @@ def get_rank(rank_id): @app.route("/ranks/", methods=["PUT"]) @adminRequired def update_rank(admin, rank_id): - """ - Update the rank with the given id. + """Update the rank with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param rank_id: Is the product id. diff --git a/shopdb/routes/replenishmentcollections.py b/src/shop_db2/routes/replenishmentcollections.py similarity index 89% rename from shopdb/routes/replenishmentcollections.py rename to src/shop_db2/routes/replenishmentcollections.py index af430e9..bf01206 100644 --- a/shopdb/routes/replenishmentcollections.py +++ b/src/shop_db2/routes/replenishmentcollections.py @@ -5,21 +5,20 @@ from flask import jsonify, request from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body, parse_timestamp -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Product, Replenishment, ReplenishmentCollection, User +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body, parse_timestamp +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Product, Replenishment, ReplenishmentCollection, User @app.route("/replenishmentcollections", methods=["GET"]) @adminRequired def list_replenishmentcollections(admin): - """ - Returns a list of all replenishmentcollections. + """Returns a list of all replenishmentcollections. :param admin: Is the administrator user, determined by @adminRequired. @@ -36,8 +35,7 @@ def list_replenishmentcollections(admin): @app.route("/replenishments", methods=["GET"]) @adminRequired def list_replenishments(admin): - """ - Returns a list of all replenishments. + """Returns a list of all replenishments. :param admin: Is the administrator user, determined by @adminRequired. @@ -54,8 +52,7 @@ def list_replenishments(admin): @app.route("/replenishmentcollections/", methods=["GET"]) @adminRequired def get_replenishmentcollection(admin, collection_id): - """ - Returns the replenishmentcollection with the requested id. In addition, + """Returns the replenishmentcollection with the requested id. In addition, all replenishments that belong to this collection are returned. :param admin: Is the administrator user, @@ -102,8 +99,7 @@ def get_replenishmentcollection(admin, collection_id): @app.route("/replenishmentcollections", methods=["POST"]) @adminRequired def create_replenishmentcollection(admin): - """ - Insert a new replenishmentcollection. + """Insert a new replenishmentcollection. :param admin: Is the administrator user, determined by @adminRequired. @@ -194,8 +190,7 @@ def create_replenishmentcollection(admin): @app.route("/replenishmentcollections/", methods=["PUT"]) @adminRequired def update_replenishmentcollection(admin, collection_id): - """ - Update the replenishmentcollection with the given id. + """Update the replenishmentcollection with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param collection_id: Is the replenishmentcollection id. @@ -208,8 +203,7 @@ def update_replenishmentcollection(admin, collection_id): @app.route("/replenishments/", methods=["PUT"]) @adminRequired def update_replenishment(admin, replenishment_id): - """ - Update the replenishment with the given id. + """Update the replenishment with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param replenishment_id: Is the replenishment id. diff --git a/shopdb/routes/stocktakingcollections.py b/src/shop_db2/routes/stocktakingcollections.py similarity index 87% rename from shopdb/routes/stocktakingcollections.py rename to src/shop_db2/routes/stocktakingcollections.py index f19bb60..8dc426e 100644 --- a/shopdb/routes/stocktakingcollections.py +++ b/src/shop_db2/routes/stocktakingcollections.py @@ -14,23 +14,21 @@ from sqlalchemy import func from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -import shopdb.helpers.products as product_helpers -from shopdb.api import app, db -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.stocktakings import _get_balance_between_stocktakings -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body, parse_timestamp -from shopdb.helpers.validators import (check_allowed_parameters, - check_fields_and_types) -from shopdb.models import Product, Stocktaking, StocktakingCollection +import shop_db2.exceptions as exc +import shop_db2.helpers.products as product_helpers +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.stocktakings import _get_balance_between_stocktakings +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body, parse_timestamp +from shop_db2.helpers.validators import check_allowed_parameters, check_fields_and_types +from shop_db2.models import Product, Stocktaking, StocktakingCollection @app.route("/stocktakingcollections/template", methods=["GET"]) def get_stocktakingcollection_template(): - """ - This route can be used to retrieve a template to print out for a + """This route can be used to retrieve a template to print out for a stocktaking. It lists all the products that must be included in the stocktaking. :return: A rendered PDF file with all products for the stocktaking. @@ -56,9 +54,7 @@ def get_stocktakingcollection_template(): for product in products: product_name = product.name theoretical_stock = product_helpers.get_theoretical_stock_of_product(product.id) - rows.append( - {"product_name": product_name, "theoretical_stock": theoretical_stock} - ) + rows.append({"product_name": product_name, "theoretical_stock": theoretical_stock}) # Render the template rendered = render_template("stocktakingcollections_template.html", rows=rows) @@ -75,8 +71,7 @@ def get_stocktakingcollection_template(): @app.route("/stocktakingcollections/balance", methods=["GET"]) @adminRequired def get_balance_between_stocktakings(admin): - """ - Returns the balance between two stocktakingcollections. + """Returns the balance between two stocktakingcollections. :param admin: Is the administrator user, determined by @adminRequired. @@ -108,8 +103,7 @@ def get_balance_between_stocktakings(admin): @app.route("/stocktakingcollections", methods=["GET"]) @adminRequired def list_stocktakingcollections(admin): - """ - Returns a list of all stocktakingcollections. + """Returns a list of all stocktakingcollections. :param admin: Is the administrator user, determined by @adminRequired. @@ -126,8 +120,7 @@ def list_stocktakingcollections(admin): @app.route("/stocktakingcollections/", methods=["GET"]) @adminRequired def get_stocktakingcollections(admin, collection_id): - """ - Returns the stocktakingcollection with the requested id. In addition, + """Returns the stocktakingcollection with the requested id. In addition, all stocktakings that belong to this collection are returned. :param admin: Is the administrator user, @@ -158,8 +151,7 @@ def get_stocktakingcollections(admin, collection_id): @app.route("/stocktakingcollections", methods=["POST"]) @adminRequired def create_stocktakingcollections(admin): - """ - Insert a new stocktakingcollection. + """Insert a new stocktakingcollection. :param admin: Is the administrator user, determined by @adminRequired. @@ -197,11 +189,7 @@ def create_stocktakingcollections(admin): raise exc.InvalidData() # Get all active product ids - products = ( - Product.query.filter(Product.active.is_(True)) - .filter(Product.countable.is_(True)) - .all() - ) + products = Product.query.filter(Product.active.is_(True)).filter(Product.countable.is_(True)).all() active_ids = list(map(lambda p: p.id, products)) data_product_ids = list(map(lambda d: d["product_id"], stocktakings)) @@ -270,8 +258,7 @@ def compare(x, y): @app.route("/stocktakingcollections/", methods=["PUT"]) @adminRequired def update_stocktakingcollection(admin, collection_id): - """ - Update the stocktakingcollection with the given id. + """Update the stocktakingcollection with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param collection_id: Is the stocktakingcollection id. @@ -284,8 +271,7 @@ def update_stocktakingcollection(admin, collection_id): @app.route("/stocktakings/", methods=["PUT"]) @adminRequired def update_stocktaking(admin, stocktaking_id): - """ - Update the stocktaking with the given id. + """Update the stocktaking with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param stocktaking_id: Is the stocktaking id. diff --git a/shopdb/routes/tagassignments.py b/src/shop_db2/routes/tagassignments.py similarity index 87% rename from shopdb/routes/tagassignments.py rename to src/shop_db2/routes/tagassignments.py index 3c6707e..4bc731d 100644 --- a/shopdb/routes/tagassignments.py +++ b/src/shop_db2/routes/tagassignments.py @@ -5,19 +5,18 @@ from flask import jsonify from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.utils import json_body -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Product, Tag +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.utils import json_body +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Product, Tag @app.route("/tagassignment/", methods=["POST"]) @adminRequired def change_product_tag_assignment(admin, command): - """ - Under this route, a tag can be added to a product or removed. + """Under this route, a tag can be added to a product or removed. :param admin: Is the administrator user, determined by @adminRequired. @@ -34,7 +33,6 @@ def change_product_tag_assignment(admin, command): :raises EntryNotFound: If the tag with the specified ID does not exist. :raises NothingHasChanged: If no change occurred after the update or removal. """ - if command not in ["add", "remove"]: raise exc.UnauthorizedAccess() diff --git a/shopdb/routes/tags.py b/src/shop_db2/routes/tags.py similarity index 87% rename from shopdb/routes/tags.py rename to src/shop_db2/routes/tags.py index afb5229..c7eaf2a 100644 --- a/shopdb/routes/tags.py +++ b/src/shop_db2/routes/tags.py @@ -5,20 +5,19 @@ from flask import jsonify, request from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.helpers.decorators import adminRequired -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.helpers.validators import check_fields_and_types -from shopdb.models import Tag +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.helpers.decorators import adminRequired +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.helpers.validators import check_fields_and_types +from shop_db2.models import Tag @app.route("/tags", methods=["GET"]) def list_tags(): - """ - Returns a list of all tags. + """Returns a list of all tags. :return: A list of all tags. """ @@ -32,8 +31,7 @@ def list_tags(): @app.route("/tags/", methods=["GET"]) def get_tag(tag_id): - """ - Returns the tag with the requested id. + """Returns the tag with the requested id. :param tag_id: Is the tag id. @@ -52,8 +50,7 @@ def get_tag(tag_id): @app.route("/tags/", methods=["DELETE"]) @adminRequired def delete_tag(admin, tag_id): - """ - Delete a tag. + """Delete a tag. :param admin: Is the administrator user, determined by @adminRequired. @@ -91,8 +88,7 @@ def delete_tag(admin, tag_id): @app.route("/tags", methods=["POST"]) @adminRequired def create_tag(admin): - """ - Route to create a new tag. + """Route to create a new tag. :param admin: Is the administrator user, determined by @adminRequired. @@ -135,8 +131,7 @@ def create_tag(admin): @app.route("/tags/", methods=["PUT"]) @adminRequired def update_tag(admin, tag_id): - """ - Update the tag with the given id. + """Update the tag with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param tag_id: Is the product id. diff --git a/shopdb/routes/users.py b/src/shop_db2/routes/users.py similarity index 87% rename from shopdb/routes/users.py rename to src/shop_db2/routes/users.py index 07cb3da..b78383d 100644 --- a/shopdb/routes/users.py +++ b/src/shop_db2/routes/users.py @@ -5,22 +5,20 @@ from flask import jsonify, request from sqlalchemy.exc import IntegrityError -import shopdb.exceptions as exc -from shopdb.api import app, bcrypt, db -from shopdb.helpers.decorators import (adminOptional, adminRequired, - checkIfUserIsValid) -from shopdb.helpers.query import QueryFromRequestParameters -from shopdb.helpers.updater import generic_update -from shopdb.helpers.users import insert_user -from shopdb.helpers.utils import convert_minimal, json_body -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import app, bcrypt, db +from shop_db2.helpers.decorators import adminOptional, adminRequired, checkIfUserIsValid +from shop_db2.helpers.query import QueryFromRequestParameters +from shop_db2.helpers.updater import generic_update +from shop_db2.helpers.users import insert_user +from shop_db2.helpers.utils import convert_minimal, json_body +from shop_db2.models import User @app.route("/users", methods=["GET"]) @adminOptional def list_users(admin): - """ - Returns a list of all users. If this route is called by an + """Returns a list of all users. If this route is called by an administrator, all information is returned. However, if it is called without further rights, a minimal version is returned. @@ -28,7 +26,6 @@ def list_users(admin): :return: A list of all users. """ - # Define fields if admin is None: fields = ["id", "firstname", "lastname", "fullname", "rank_id", "imagename"] @@ -66,8 +63,7 @@ def list_users(admin): @app.route("/users", methods=["POST"]) def create_user(): - """ - Registration of new users. + """Registration of new users. :return: A message that the registration was successful. @@ -85,8 +81,7 @@ def create_user(): @app.route("/users//favorites", methods=["GET"]) @checkIfUserIsValid def get_user_favorites(user, user_id): - """ - Returns a list with the IDs of a user's favorite products. The list is + """Returns a list with the IDs of a user's favorite products. The list is empty if no favourite products exist. :param user: Is the user, determined by @checkIfUserIsValid. @@ -94,22 +89,19 @@ def get_user_favorites(user, user_id): :return: A list with the IDs of the favorite products. """ - return jsonify(user.favorites), 200 @app.route("/users//deposits", methods=["GET"]) @checkIfUserIsValid def get_user_deposits(user, user_id): - """ - Returns a list with all deposits of a user. + """Returns a list with all deposits of a user. :param user: Is the user, determined by @checkIfUserIsValid. :param user_id: Is the user id. :return: A list with all deposits of the user. """ - fields = ["id", "timestamp", "admin_id", "amount", "revoked", "comment"] deposits = convert_minimal(user.deposits.all(), fields) @@ -119,15 +111,13 @@ def get_user_deposits(user, user_id): @app.route("/users//replenishmentcollections", methods=["GET"]) @checkIfUserIsValid def get_user_replenishmentcollections(user, user_id): - """ - Returns a list with all replenishmentcollections of a user. + """Returns a list with all replenishmentcollections of a user. :param user: Is the user, determined by @checkIfUserIsValid. :param user_id: Is the user id. :return: A list with all replenishmentcollections of the user. """ - fields = ["id", "timestamp", "admin_id", "price", "revoked", "comment"] deposits = convert_minimal(user.replenishmentcollections.all(), fields) @@ -137,15 +127,13 @@ def get_user_replenishmentcollections(user, user_id): @app.route("/users//purchases", methods=["GET"]) @checkIfUserIsValid def get_user_purchases(user, user_id): - """ - Returns a list with all purchases of a user. + """Returns a list with all purchases of a user. :param user: Is the user, determined by @checkIfUserIsValid. :param user_id: Is the user id. :return: A list with all purchases of the user. """ - fields = [ "id", "timestamp", @@ -164,8 +152,7 @@ def get_user_purchases(user, user_id): @app.route("/users/", methods=["GET"]) @adminOptional def get_user(admin, user_id): - """ - Returns the user with the requested id. + """Returns the user with the requested id. :param admin: Is the administrator user, determined by @adminOptional. :param user_id: Is the user id. @@ -206,8 +193,7 @@ def get_user(admin, user_id): @app.route("/users/", methods=["PUT"]) @adminRequired def update_user(admin, user_id): - """ - Update the user with the given id. + """Update the user with the given id. :param admin: Is the administrator user, determined by @adminRequired. :param user_id: Is the user id. @@ -233,9 +219,7 @@ def update_user(admin, user_id): raise exc.DataIsMissing() # Both must be strings - if not all( - [isinstance(x, str) for x in [data["password"], data["password_repeat"]]] - ): + if not all([isinstance(x, str) for x in [data["password"], data["password_repeat"]]]): raise exc.WrongType() # Passwords must match @@ -260,8 +244,7 @@ def update_user(admin, user_id): @app.route("/users/", methods=["DELETE"]) @adminRequired def delete_user(admin, user_id): - """ - Delete a user. This is only possible if the user has not yet been verified. + """Delete a user. This is only possible if the user has not yet been verified. :param admin: Is the administrator user, determined by @adminRequired. diff --git a/shopdb/shared/__init__.py b/src/shop_db2/shared/__init__.py similarity index 100% rename from shopdb/shared/__init__.py rename to src/shop_db2/shared/__init__.py diff --git a/shopdb/templates/stocktakingcollections_template.html b/src/shop_db2/templates/stocktakingcollections_template.html similarity index 100% rename from shopdb/templates/stocktakingcollections_template.html rename to src/shop_db2/templates/stocktakingcollections_template.html diff --git a/shopdb/uploads/broken_image.jpeg b/src/shop_db2/uploads/broken_image.jpeg similarity index 100% rename from shopdb/uploads/broken_image.jpeg rename to src/shop_db2/uploads/broken_image.jpeg diff --git a/shopdb/uploads/broken_image.jpg b/src/shop_db2/uploads/broken_image.jpg similarity index 100% rename from shopdb/uploads/broken_image.jpg rename to src/shop_db2/uploads/broken_image.jpg diff --git a/shopdb/uploads/broken_image.png b/src/shop_db2/uploads/broken_image.png similarity index 100% rename from shopdb/uploads/broken_image.png rename to src/shop_db2/uploads/broken_image.png diff --git a/shopdb/uploads/default.png b/src/shop_db2/uploads/default.png similarity index 100% rename from shopdb/uploads/default.png rename to src/shop_db2/uploads/default.png diff --git a/shopdb/uploads/non_quadratic.png b/src/shop_db2/uploads/non_quadratic.png similarity index 100% rename from shopdb/uploads/non_quadratic.png rename to src/shop_db2/uploads/non_quadratic.png diff --git a/shopdb/uploads/valid_image.jpeg b/src/shop_db2/uploads/valid_image.jpeg similarity index 100% rename from shopdb/uploads/valid_image.jpeg rename to src/shop_db2/uploads/valid_image.jpeg diff --git a/shopdb/uploads/valid_image.jpg b/src/shop_db2/uploads/valid_image.jpg similarity index 100% rename from shopdb/uploads/valid_image.jpg rename to src/shop_db2/uploads/valid_image.jpg diff --git a/shopdb/uploads/valid_image.png b/src/shop_db2/uploads/valid_image.png similarity index 100% rename from shopdb/uploads/valid_image.png rename to src/shop_db2/uploads/valid_image.png diff --git a/test.py b/test.py deleted file mode 100755 index 4e90ced..0000000 --- a/test.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -__author__ = "g3n35i5" - -import os -import sys -import unittest -from argparse import ArgumentParser - - -def do_all_tests(): - tests = unittest.TestLoader().discover("tests") - unittest.TextTestRunner(verbosity=2).run(tests) - - -if __name__ == "__main__": - - parser = ArgumentParser(description="Running unittests for shop.db.") - parser.add_argument( - "--mode", - help="Select the operating mode.", - default="interactive", - choices=["interactive", "auto"], - ) - - args = parser.parse_args() - - if args.mode == "interactive": # pragma: no cover - list_files = os.listdir("tests") - list_tests = [] - - for file in list_files: - try: - file.index("test") - except ValueError: - continue - else: - list_tests.append(file) - list_tests = sorted(list_tests) - - print("Please select the tests you want to run. [Default=all]") - print("all: All tests") - for i, test in enumerate(list_tests): - print("{:3d}: {}".format(i, test)) - answ = input("Testnumbers: ") - - if answ in ["", "all", "a"]: - do_all_tests() - else: - list_testnumbers = answ.split(" ") - test = [] - - for i, testnumber in enumerate(list_testnumbers): - try: - testnumber = int(testnumber) - - except ValueError: - sys.exit("Invalid input") - - if testnumber not in range(0, len(list_tests)): - sys.exit("Invalid input") - - test.append( - unittest.TestLoader().discover( - "tests", pattern=list_tests[testnumber] - ) - ) - - testcombo = unittest.TestSuite(test) - unittest.TextTestRunner(verbosity=2).run(testcombo) - elif args.mode == "auto": - do_all_tests() - - else: # pragma: no cover - sys.exit(f"Invalid operating mode: {args.mode}") diff --git a/tests/__init__.py b/tests/__init__.py index 5201689..35ffd1d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -__author__ = "g3n35i5" +"""This package contains resources, utility functions, and the tests of the test suite.""" diff --git a/tests/base.py b/tests/base.py index e16c1e7..8cf63ae 100644 --- a/tests/base.py +++ b/tests/base.py @@ -4,11 +4,22 @@ from flask_testing import TestCase -import configuration as config -from shopdb.api import app, bcrypt, db, set_app -from shopdb.models import (AdminUpdate, Deposit, Product, Purchase, Rank, - Replenishment, ReplenishmentCollection, Stocktaking, - StocktakingCollection, Tag, User) +from shop_db2.api import app, bcrypt, db, set_app +from shop_db2.models import ( + AdminUpdate, + Deposit, + Product, + Purchase, + Rank, + Replenishment, + ReplenishmentCollection, + Stocktaking, + StocktakingCollection, + Tag, + User, +) + +import configuration as config # isort: skip # Global password storage. Hashing the passwords for each unit test # would take too long. For this reason, the passwords are created once @@ -75,7 +86,8 @@ def tearDown(self): def generate_passwords(self, password_list): """This function generates hashes of passwords and stores them in the - global variable so that they do not have to be created again.""" + global variable so that they do not have to be created again. + """ global passwords if passwords is None: passwords = [None] * len(password_list) @@ -151,12 +163,8 @@ def insert_default_replenishmentcollections(): product1 = Product.query.filter_by(id=1).first() product2 = Product.query.filter_by(id=2).first() product3 = Product.query.filter_by(id=3).first() - rc1 = ReplenishmentCollection( - admin_id=1, revoked=False, comment="Foo", seller_id=5 - ) - rc2 = ReplenishmentCollection( - admin_id=2, revoked=False, comment="Foo", seller_id=5 - ) + rc1 = ReplenishmentCollection(admin_id=1, revoked=False, comment="Foo", seller_id=5) + rc2 = ReplenishmentCollection(admin_id=2, revoked=False, comment="Foo", seller_id=5) for r in [rc1, rc2]: db.session.add(r) db.session.flush() diff --git a/tests/base_api.py b/tests/base_api.py index e364f14..c9e9425 100644 --- a/tests/base_api.py +++ b/tests/base_api.py @@ -13,7 +13,8 @@ class BaseAPITestCase(BaseTestCase): def assertException(self, res, exception): """This helper function checks whether the correct exception has - been raised""" + been raised + """ data = json.loads(res.data) self.assertEqual(res.status_code, exception.code) self.assertEqual(data["message"], exception.message) @@ -67,9 +68,7 @@ def post(self, url, data=None, role=None, content_type="application/json"): content_type=content_type, ) - def get( - self, url, data=None, role=None, params=None, content_type="application/json" - ): + def get(self, url, data=None, role=None, params=None, content_type="application/json"): """Helper function to perform a GET request to the API""" return self._request( request_type="GET", @@ -82,9 +81,7 @@ def get( def put(self, url, data=None, role=None, content_type="application/json"): """Helper function to perform a GET request to the API""" - return self._request( - request_type="PUT", url=url, data=data, role=role, content_type=content_type - ) + return self._request(request_type="PUT", url=url, data=data, role=role, content_type=content_type) def delete(self, url, data=None, role=None, content_type="application/json"): """Helper function to perform a DELETE request to the API""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f090b93 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +"""This module contains pytest fixtures.""" + +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session", name="resource_dir") +def resource_dir_fixture() -> Path: + """Returns the path to the test resource directory. + + Returns: + Path: The resource directory path. + """ + return Path(__file__).parent.joinpath("resources") diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..80cd825 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""This sub-package contains integration tests to test components of your package in combination.""" diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py new file mode 100644 index 0000000..09d9924 --- /dev/null +++ b/tests/resources/__init__.py @@ -0,0 +1 @@ +"""This sub-package contains test resources that are needed for your tests (e.g., images).""" diff --git a/tests/test_default_data.py b/tests/test_default_data.py index 69a2182..03502ea 100644 --- a/tests/test_default_data.py +++ b/tests/test_default_data.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -from shopdb.api import bcrypt -from shopdb.models import Rank, User +from shop_db2.api import bcrypt +from shop_db2.models import Rank, User from tests.base import BaseTestCase, rank_data, user_data @@ -18,9 +18,7 @@ def test_default_users(self): self.assertEqual(users[index].firstname, data["firstname"]) self.assertEqual(users[index].lastname, data["lastname"]) if data["password"]: - self.assertTrue( - bcrypt.check_password_hash(users[index].password, data["password"]) - ) + self.assertTrue(bcrypt.check_password_hash(users[index].password, data["password"])) def test_insert_default_ranks(self): """Check if all ranks have been entered correctly""" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..b9f707b --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,4 @@ +"""This sub-package contains unit tests to test components of your package in isolation. + +The folder structure should reflect the src folder layout. +""" diff --git a/tests/unit/api/__init__.py b/tests/unit/api/__init__.py new file mode 100644 index 0000000..aedf650 --- /dev/null +++ b/tests/unit/api/__init__.py @@ -0,0 +1 @@ +"""Contains all unit tests for the API.""" diff --git a/tests/test_api_change_tagassignment.py b/tests/unit/api/test_api_change_tagassignment.py similarity index 95% rename from tests/test_api_change_tagassignment.py rename to tests/unit/api/test_api_change_tagassignment.py index 68a747e..92ec793 100644 --- a/tests/test_api_change_tagassignment.py +++ b/tests/unit/api/test_api_change_tagassignment.py @@ -4,11 +4,12 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -# from shopdb.models import Product, Tag -from shopdb.models.product import Product -from shopdb.models.tag import Tag +import shop_db2.exceptions as exc +from shop_db2.api import db + +# from shop_db2.models import Product, Tag +from shop_db2.models.product import Product +from shop_db2.models.tag import Tag from tests.base_api import BaseAPITestCase @@ -118,9 +119,7 @@ def test_assign_tag_unknown_field(self): self.assertEqual(0, len(Product.query.filter_by(id=1).first().tags)) def test_assign_tag_invalid_command(self): - """ - Invalid command should raise an exception. - """ + """Invalid command should raise an exception.""" data = {"product_id": 1, "tag_id": 1, "foo": 42} res = self.post(url="/tagassignment/foo", role="admin", data=data) self.assertEqual(res.status_code, 401) diff --git a/tests/test_api_create_batch_deposit.py b/tests/unit/api/test_api_create_batch_deposit.py similarity index 97% rename from tests/test_api_create_batch_deposit.py rename to tests/unit/api/test_api_create_batch_deposit.py index a003e9f..cb87190 100644 --- a/tests/test_api_create_batch_deposit.py +++ b/tests/unit/api/test_api_create_batch_deposit.py @@ -6,9 +6,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Deposit, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Deposit, User from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_create_deposit.py b/tests/unit/api/test_api_create_deposit.py similarity index 97% rename from tests/test_api_create_deposit.py rename to tests/unit/api/test_api_create_deposit.py index cb8632a..0cc1bac 100644 --- a/tests/test_api_create_deposit.py +++ b/tests/unit/api/test_api_create_deposit.py @@ -6,9 +6,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Deposit, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Deposit, User from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_create_product.py b/tests/unit/api/test_api_create_product.py similarity index 93% rename from tests/test_api_create_product.py rename to tests/unit/api/test_api_create_product.py index b3a3f7b..6629e4d 100644 --- a/tests/test_api_create_product.py +++ b/tests/unit/api/test_api_create_product.py @@ -6,9 +6,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product from tests.base_api import BaseAPITestCase @@ -73,8 +73,7 @@ def test_create_product_wrong_type(self): self.assertEqual(len(Product.query.all()), 4) def test_create_product_wrong_type_tags(self): - """ - If the tags of a product are of the wrong type, an exception must + """If the tags of a product are of the wrong type, an exception must be raised. """ data = {"name": "Bread", "price": 100, "tags": ["1"]} @@ -84,9 +83,7 @@ def test_create_product_wrong_type_tags(self): self.assertFalse(Product.query.filter_by(id=5).first()) def test_create_product_non_existing_tag(self): - """ - If the tags of a product do not exist, an exception must be raised. - """ + """If the tags of a product do not exist, an exception must be raised.""" data = {"name": "Bread", "price": 100, "tags": [42]} res = self.post(url="/products", role="admin", data=data) self.assertEqual(res.status_code, 401) @@ -126,9 +123,7 @@ def test_create_product_with_existing_name(self): self.assertFalse(Product.query.filter_by(id=5).first()) def test_create_product_already_existing(self): - """ - Creating a product with an existing barcode should not be possible. - """ + """Creating a product with an existing barcode should not be possible.""" Product.query.filter_by(id=1).first().barcode = "123456" db.session.commit() data = {"name": "FooBar", "price": 100, "barcode": "123456", "tags": [1]} diff --git a/tests/test_api_create_purchase.py b/tests/unit/api/test_api_create_purchase.py similarity index 96% rename from tests/test_api_create_purchase.py rename to tests/unit/api/test_api_create_purchase.py index b3d950c..5279b44 100644 --- a/tests/test_api_create_purchase.py +++ b/tests/unit/api/test_api_create_purchase.py @@ -6,9 +6,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, Purchase, Rank, Tag, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, Purchase, Rank, Tag, User from tests.base_api import BaseAPITestCase @@ -50,8 +50,7 @@ def test_create_purchase_insufficient_credit_contender(self): self.assertException(res, exc.InsufficientCredit) def test_create_purchase_insufficient_credit_by_admin(self): - """ - If the purchase is made by an administrator, the credit limit + """If the purchase is made by an administrator, the credit limit may be exceeded. """ data = {"user_id": 3, "product_id": 3, "amount": 4} @@ -114,7 +113,8 @@ def test_create_multiple_purchases_at_once(self): def test_create_multiple_purchases_with_invalid_data(self): """This test ensures that if you create multiple purchases at once, a single error will - prevent the whole transaction""" + prevent the whole transaction + """ data = [ {"user_id": 2, "product_id": 1, "amount": 1}, {"user_id": 2, "product_id": 2, "amount": 1}, @@ -151,9 +151,7 @@ def test_create_purchase_with_timestamp_without_admin_permissions(self): self.assertException(res, exc.ForbiddenField) def test_create_purchase_product_not_for_sale(self): - """ - Creating a purchase with a product which is not for sale must raise an exception - """ + """Creating a purchase with a product which is not for sale must raise an exception""" # Assign a "not for sale" tag to the product tag = db.session.query(Tag).filter(Tag.is_for_sale.is_(False)).first() product = Product.query.filter_by(id=1).first() @@ -226,8 +224,7 @@ def test_create_purchase_inactive_product(self): self.assertEqual(len(Purchase.query.all()), 0) def test_create_purchase_inactive_product_by_admin(self): - """ - If the purchase is made by an administrator, the product is allowed + """If the purchase is made by an administrator, the product is allowed to be inactive. """ product = Product.query.filter_by(id=4).first() diff --git a/tests/test_api_create_rank.py b/tests/unit/api/test_api_create_rank.py similarity index 81% rename from tests/test_api_create_rank.py rename to tests/unit/api/test_api_create_rank.py index e61ff33..01ddd18 100644 --- a/tests/test_api_create_rank.py +++ b/tests/unit/api/test_api_create_rank.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Rank +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Rank from tests.base_api import BaseAPITestCase @@ -25,7 +25,6 @@ def test_create_rank_authorization(self): def test_create_rank(self): """Create a rank as admin.""" - rank_data_list = [ {"name": "Rank1"}, {"name": "Rank2", "is_system_user": True}, @@ -38,24 +37,9 @@ def test_create_rank(self): data = json.loads(res.data) self.assertEqual(data["message"], "Created Rank.") - self.assertFalse( - db.session.query(Rank) - .filter(Rank.name == rank_data_list[0]["name"]) - .first() - .is_system_user - ) - self.assertTrue( - db.session.query(Rank) - .filter(Rank.name == rank_data_list[1]["name"]) - .first() - .is_system_user - ) - self.assertFalse( - db.session.query(Rank) - .filter(Rank.name == rank_data_list[2]["name"]) - .first() - .is_system_user - ) + self.assertFalse(db.session.query(Rank).filter(Rank.name == rank_data_list[0]["name"]).first().is_system_user) + self.assertTrue(db.session.query(Rank).filter(Rank.name == rank_data_list[1]["name"]).first().is_system_user) + self.assertFalse(db.session.query(Rank).filter(Rank.name == rank_data_list[2]["name"]).first().is_system_user) def test_create_rank_wrong_type(self): """Create a rank as admin with wrong type(s).""" diff --git a/tests/test_api_create_replenismentcollection.py b/tests/unit/api/test_api_create_replenismentcollection.py similarity index 95% rename from tests/test_api_create_replenismentcollection.py rename to tests/unit/api/test_api_create_replenismentcollection.py index b72b307..12c5a6e 100644 --- a/tests/test_api_create_replenismentcollection.py +++ b/tests/unit/api/test_api_create_replenismentcollection.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, ReplenishmentCollection, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, ReplenishmentCollection, User from tests.base_api import BaseAPITestCase @@ -65,8 +65,7 @@ def test_check_users_credit_after_inserting_replenishmentcollection(self): self.assertEqual(sum_collections - price, seller.credit) def test_create_replenishmentcollection_reactivate_product(self): - """ - If a product was marked as inactive with a stocktaking, it can + """If a product was marked as inactive with a stocktaking, it can be set to active again with a replenishment. This functionality is checked with this test. """ @@ -160,8 +159,7 @@ def test_create_replenishmentcollection_with_missing_data_V(self): self.assertException(res, exc.DataIsMissing) def test_create_replenishmentcollection_with_unknown_field_I(self): - """ - Creating a replenishmentcollection with unknown field in the + """Creating a replenishmentcollection with unknown field in the collection itself should raise an exception. """ replenishments = [ @@ -180,8 +178,7 @@ def test_create_replenishmentcollection_with_unknown_field_I(self): self.assertException(res, exc.UnknownField) def test_create_replenishmentcollection_with_unknown_field_II(self): - """ - Creating a replenishmentcollection with unknown field in one of the + """Creating a replenishmentcollection with unknown field in one of the replenishments should raise an exception. """ replenishments = [ @@ -199,8 +196,7 @@ def test_create_replenishmentcollection_with_unknown_field_II(self): self.assertException(res, exc.UnknownField) def test_create_replenishmentcollection_with_wrong_type_I(self): - """ - Creating a replenishmentcollection with wrong type in the + """Creating a replenishmentcollection with wrong type in the replenishmentcollection itself should raise an exception. """ replenishments = [ @@ -218,8 +214,7 @@ def test_create_replenishmentcollection_with_wrong_type_I(self): self.assertException(res, exc.WrongType) def test_create_replenishmentcollection_with_wrong_type_II(self): - """ - Creating a replenishmentcollection with wrong type in one of the + """Creating a replenishmentcollection with wrong type in one of the replenishments should raise an exception. """ replenishments = [ diff --git a/tests/test_api_create_stocktakingcollection.py b/tests/unit/api/test_api_create_stocktakingcollection.py similarity index 93% rename from tests/test_api_create_stocktakingcollection.py rename to tests/unit/api/test_api_create_stocktakingcollection.py index d00e53a..5bae8b8 100644 --- a/tests/test_api_create_stocktakingcollection.py +++ b/tests/unit/api/test_api_create_stocktakingcollection.py @@ -6,9 +6,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, StocktakingCollection +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, StocktakingCollection from tests.base_api import BaseAPITestCase @@ -27,7 +27,6 @@ def test_authorization(self): def test_create_stocktaking_collection_as_admin(self): """Creating a StocktakingCollection as admin""" - self.insert_default_stocktakingcollections() # Set product 1 to non countable @@ -63,9 +62,7 @@ def test_create_stocktaking_collection_as_admin(self): self.assertEqual(getattr(api_stocktakings[i], key), _dict[key]) def test_create_stocktakingcollection_non_countable_product(self): - """ - Only the products which are countable can be in a stocktaking. - """ + """Only the products which are countable can be in a stocktaking.""" # Set product 1 to non countable Product.query.filter_by(id=1).first().countable = False db.session.commit() @@ -87,8 +84,7 @@ def test_create_stocktakingcollection_non_countable_product(self): self.assertFalse(StocktakingCollection.query.all()) def test_create_stocktakingcollection_non_existing_product(self): - """ - If a product does not exist of an stocktakingcollection, an exception + """If a product does not exist of an stocktakingcollection, an exception must be raised. """ stocktakings = [{"product_id": 42, "count": 100}] @@ -101,8 +97,7 @@ def test_create_stocktakingcollection_non_existing_product(self): self.assertException(res, exc.EntryNotFound) def test_create_stocktakingcollection_set_product_inactive(self): - """ - This test ensures that a product is set to inactive if this is + """This test ensures that a product is set to inactive if this is specified in the stocktaking. """ stocktakings = [ @@ -145,8 +140,7 @@ def test_create_stocktakingcollection_with_missing_data_III(self): self.assertException(res, exc.DataIsMissing) def test_create_stocktakingcollection_with_unknown_field_I(self): - """ - Creating a stocktakingcollection with unknown field in the + """Creating a stocktakingcollection with unknown field in the collection itself should raise an exception. """ stocktakings = [ @@ -165,8 +159,7 @@ def test_create_stocktakingcollection_with_unknown_field_I(self): self.assertException(res, exc.UnknownField) def test_create_stocktakingcollection_with_unknown_field_II(self): - """ - Creating a stocktakingcollection with unknown field in one of the + """Creating a stocktakingcollection with unknown field in one of the stocktakings should raise an exception. """ stocktakings = [ @@ -184,8 +177,7 @@ def test_create_stocktakingcollection_with_unknown_field_II(self): self.assertException(res, exc.UnknownField) def test_create_stocktakingcollection_with_wrong_type_I(self): - """ - Creating a stocktakingcollection with wrong type in the + """Creating a stocktakingcollection with wrong type in the stocktakingcollection itself should raise an exception. """ data = {"stocktakings": 42, "timestamp": int(self.TIMESTAMP.timestamp())} @@ -194,8 +186,7 @@ def test_create_stocktakingcollection_with_wrong_type_I(self): self.assertException(res, exc.WrongType) def test_create_stocktakingcollection_with_wrong_type_II(self): - """ - Creating a stocktakingcollection with wrong type in one of the + """Creating a stocktakingcollection with wrong type in one of the stocktakings should raise an exception. """ stocktakings = [ diff --git a/tests/test_api_create_tag.py b/tests/unit/api/test_api_create_tag.py similarity index 86% rename from tests/test_api_create_tag.py rename to tests/unit/api/test_api_create_tag.py index 58cf1d8..ba48b1b 100644 --- a/tests/test_api_create_tag.py +++ b/tests/unit/api/test_api_create_tag.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Tag +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Tag from tests.base_api import BaseAPITestCase @@ -25,7 +25,6 @@ def test_create_tag_authorization(self): def test_create_tag(self): """Create a tag as admin.""" - tag_data_list = [ {"name": "CoolTag"}, {"name": "CoolTag2", "is_for_sale": False}, @@ -39,18 +38,8 @@ def test_create_tag(self): tag = Tag.query.filter_by(name=tag_data["name"]).first() self.assertEqual(tag.created_by, 1) - self.assertTrue( - db.session.query(Tag) - .filter(Tag.name == tag_data_list[0]["name"]) - .first() - .is_for_sale - ) - self.assertFalse( - db.session.query(Tag) - .filter(Tag.name == tag_data_list[1]["name"]) - .first() - .is_for_sale - ) + self.assertTrue(db.session.query(Tag).filter(Tag.name == tag_data_list[0]["name"]).first().is_for_sale) + self.assertFalse(db.session.query(Tag).filter(Tag.name == tag_data_list[1]["name"]).first().is_for_sale) def test_create_tag_wrong_type(self): """Create a tag as admin with wrong type(s).""" diff --git a/tests/test_api_create_user.py b/tests/unit/api/test_api_create_user.py similarity index 90% rename from tests/test_api_create_user.py rename to tests/unit/api/test_api_create_user.py index 00747cd..1886417 100644 --- a/tests/test_api_create_user.py +++ b/tests/unit/api/test_api_create_user.py @@ -4,8 +4,8 @@ from copy import copy -import shopdb.exceptions as exc -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.models import User from tests.base_api import BaseAPITestCase @@ -29,9 +29,7 @@ def test_create_user(self): self.assertFalse(user.is_verified) def test_create_user_only_lastname(self): - """ - It should be possible to create a user without a firstname. - """ + """It should be possible to create a user without a firstname.""" data = {"lastname": "Doe"} res = self.post(url="/users", data=data) self.assertEqual(res.status_code, 200) @@ -43,7 +41,8 @@ def test_create_user_only_lastname(self): def test_create_user_password_too_short(self): """This test should ensure that the correct exception gets returned - on creating a user with a short password.""" + on creating a user with a short password. + """ data = { "firstname": "John", "lastname": "Doe", @@ -58,7 +57,8 @@ def test_create_user_password_too_short(self): def test_create_user_missing_data(self): """This test should ensure that the correct exception gets returned - on creating a user with missing data.""" + on creating a user with missing data. + """ data = {"firstname": "John"} res = self.post(url="/users", data=data) self.assertException(res, exc.DataIsMissing) @@ -68,8 +68,8 @@ def test_create_user_missing_data(self): def test_create_user_wrong_type(self): """This test should ensure that the correct exception gets returned - on creating a user with a wrong data type.""" - + on creating a user with a wrong data type. + """ data = { "firstname": "John", "lastname": "Doe", @@ -88,7 +88,8 @@ def test_create_user_wrong_type(self): def test_create_user_passwords_do_not_match(self): """This test should ensure that the correct exception gets returned - on creating a user when the passwords do not match.""" + on creating a user when the passwords do not match. + """ data = { "firstname": "John", "lastname": "Doe", @@ -103,7 +104,8 @@ def test_create_user_passwords_do_not_match(self): def test_create_user_passwords_repeat_is_missing(self): """This test should ensure that the correct exception gets returned - on creating a user when the password_repeat field is missing.""" + on creating a user when the password_repeat field is missing. + """ data = {"firstname": "John", "lastname": "Doe", "password": "supersecret"} res = self.post(url="/users", data=data) self.assertException(res, exc.DataIsMissing) diff --git a/tests/test_api_delete_tag.py b/tests/unit/api/test_api_delete_tag.py similarity index 91% rename from tests/test_api_delete_tag.py rename to tests/unit/api/test_api_delete_tag.py index c7d24d7..ffa8a44 100644 --- a/tests/test_api_delete_tag.py +++ b/tests/unit/api/test_api_delete_tag.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, Tag +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, Tag from tests.base_api import BaseAPITestCase @@ -30,8 +30,7 @@ def test_delete_tag(self): self.assertEqual(tag, None) def test_delete_assigned_tag(self): - """ - If a tag that is already assigned to products is deleted, it must be + """If a tag that is already assigned to products is deleted, it must be checked whether it also disappears from the list of tags. """ product1 = Product.query.filter_by(id=1).first() @@ -54,8 +53,7 @@ def test_delete_assigned_tag(self): self.assertEqual(len(product2.tags), 1) def test_delete_last_tag_of_product(self): - """ - It should not be possible to delete a tag which is assigned to a + """It should not be possible to delete a tag which is assigned to a product which has only one tag. """ product = Product.query.filter_by(id=1).first() diff --git a/tests/test_api_delete_user.py b/tests/unit/api/test_api_delete_user.py similarity index 95% rename from tests/test_api_delete_user.py rename to tests/unit/api/test_api_delete_user.py index 039ad84..ccf9e50 100644 --- a/tests/test_api_delete_user.py +++ b/tests/unit/api/test_api_delete_user.py @@ -4,8 +4,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.models import User from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_get_balance_between_stocktakings.py b/tests/unit/api/test_api_get_balance_between_stocktakings.py similarity index 91% rename from tests/test_api_get_balance_between_stocktakings.py rename to tests/unit/api/test_api_get_balance_between_stocktakings.py index aa5efea..5c065cf 100644 --- a/tests/test_api_get_balance_between_stocktakings.py +++ b/tests/unit/api/test_api_get_balance_between_stocktakings.py @@ -4,15 +4,14 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase -from tests.test_helpers_stocktakings import TestHelpersStocktakingsTestCase +from tests.unit.helpers.test_helpers_stocktakings import TestHelpersStocktakingsTestCase class GetBalanceBetweenStocktakingsAPITestCase(BaseAPITestCase): def test_get_balance_between_stocktakings(self): - """ - This test ensures that the data returned by the API route for the + """This test ensures that the data returned by the API route for the balance between two stocktakingcollections is correct. """ # The data required to generate the test case can be reused from the @@ -71,8 +70,7 @@ def test_get_balance_between_stocktakings(self): self.assertEqual(balance["profit"], 0) def test_get_balance_between_stocktakings_missing_params(self): - """ - This test ensures that the correct exceptions get raised when the + """This test ensures that the correct exceptions get raised when the request params are missing. """ # Do a request without any params @@ -91,8 +89,7 @@ def test_get_balance_between_stocktakings_missing_params(self): self.assertException(res, exc.InvalidData) def test_get_balance_between_stocktakings_invalid_params(self): - """ - This test ensures that the correct exceptions get raised when the + """This test ensures that the correct exceptions get raised when the request params are invalid. """ # Do a request with only the end id given. diff --git a/tests/test_api_get_deposit.py b/tests/unit/api/test_api_get_deposit.py similarity index 94% rename from tests/test_api_get_deposit.py rename to tests/unit/api/test_api_get_deposit.py index 7511306..d6d3194 100644 --- a/tests/test_api_get_deposit.py +++ b/tests/unit/api/test_api_get_deposit.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import DepositRevoke +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import DepositRevoke from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_get_financial_overview.py b/tests/unit/api/test_api_get_financial_overview.py similarity index 84% rename from tests/test_api_get_financial_overview.py rename to tests/unit/api/test_api_get_financial_overview.py index e3b4f09..315551c 100644 --- a/tests/test_api_get_financial_overview.py +++ b/tests/unit/api/test_api_get_financial_overview.py @@ -6,17 +6,15 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import (Deposit, ProductPrice, Purchase, - ReplenishmentCollection) +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Deposit, ProductPrice, Purchase, ReplenishmentCollection from tests.base_api import BaseAPITestCase class GetFinancialOverviewAPITestCase(BaseAPITestCase): def test_authorization_get_financial_overview(self): - """ - This route may only be accessible to administrators. An exception must + """This route may only be accessible to administrators. An exception must be made for all other requests. """ res = self.get(url="/financial_overview") @@ -27,14 +25,12 @@ def test_authorization_get_financial_overview(self): self.assertException(res, exc.UnauthorizedAccess) def test_get_financial_overview(self): - """ - This test ensures that the entire financial overview is calculated + """This test ensures that the entire financial overview is calculated correctly. To do this, some test entries are entered into the database, some of which are revoked. Then the amount is manually calculated which should come out at the end of the calculation and compared with the amount calculated by the API. """ - # Add a product with negative price p_data = {"name": "Negative", "price": -100, "tags": [1]} self.post(url="/products", role="admin", data=p_data) @@ -60,16 +56,10 @@ def test_get_financial_overview(self): # Insert some purchases (some are revoked) t = datetime.strptime("2018-01-01 10:00:00", "%Y-%m-%d %H:%M:%S") p1 = Purchase(user_id=1, product_id=3, amount=4, revoked=True, timestamp=t) - p2 = Purchase( - user_id=2, product_id=2, amount=3, revoked=False, timestamp=t - ) # <- - p3 = Purchase( - user_id=3, product_id=1, amount=2, revoked=False, timestamp=t - ) # <- + p2 = Purchase(user_id=2, product_id=2, amount=3, revoked=False, timestamp=t) # <- + p3 = Purchase(user_id=3, product_id=1, amount=2, revoked=False, timestamp=t) # <- p4 = Purchase(user_id=1, product_id=2, amount=1, revoked=True, timestamp=t) - p5 = Purchase( - user_id=1, product_id=5, amount=1, revoked=False, timestamp=t - ) # <- + p5 = Purchase(user_id=1, product_id=5, amount=1, revoked=False, timestamp=t) # <- for p in [p1, p2, p3, p4, p5]: db.session.add(p) @@ -78,17 +68,11 @@ def test_get_financial_overview(self): self.assertEqual(psum, 650) # Insert some deposits (some are revoked) - d1 = Deposit( - user_id=1, admin_id=1, comment="Foo", amount=100, revoked=False - ) # <- + d1 = Deposit(user_id=1, admin_id=1, comment="Foo", amount=100, revoked=False) # <- d2 = Deposit(user_id=2, admin_id=1, comment="Foo", amount=500, revoked=True) - d3 = Deposit( - user_id=3, admin_id=1, comment="Foo", amount=300, revoked=False - ) # <- + d3 = Deposit(user_id=3, admin_id=1, comment="Foo", amount=300, revoked=False) # <- d4 = Deposit(user_id=2, admin_id=1, comment="Foo", amount=200, revoked=True) - d5 = Deposit( - user_id=2, admin_id=1, comment="Negative", amount=-100, revoked=False - ) + d5 = Deposit(user_id=2, admin_id=1, comment="Negative", amount=-100, revoked=False) for d in [d1, d2, d3, d4, d5]: db.session.add(d) @@ -172,9 +156,7 @@ def test_get_financial_overview(self): self.assertEqual(api_incomes[1]["name"], "Deposits") self.assertEqual(api_incomes[1]["amount"], positive_deposits_amount) self.assertEqual(api_incomes[2]["name"], "Replenishments") - self.assertEqual( - api_incomes[2]["amount"], negative_replenishmentcollections_price - ) + self.assertEqual(api_incomes[2]["amount"], negative_replenishmentcollections_price) self.assertEqual(api_incomes[3]["name"], "Stocktakings") self.assertEqual(api_incomes[3]["amount"], profit_between_stocktakings) @@ -185,8 +167,6 @@ def test_get_financial_overview(self): self.assertEqual(api_incomes[1]["name"], "Deposits") self.assertEqual(api_incomes[1]["amount"], negative_deposits_amount) self.assertEqual(api_incomes[2]["name"], "Replenishments") - self.assertEqual( - api_incomes[2]["amount"], positive_replenishmentcollections_price - ) + self.assertEqual(api_incomes[2]["amount"], positive_replenishmentcollections_price) self.assertEqual(api_incomes[3]["name"], "Stocktakings") self.assertEqual(api_incomes[3]["amount"], loss_between_stocktakings) diff --git a/tests/test_api_get_image.py b/tests/unit/api/test_api_get_image.py similarity index 80% rename from tests/test_api_get_image.py rename to tests/unit/api/test_api_get_image.py index ac40ca6..5cc4bf0 100644 --- a/tests/test_api_get_image.py +++ b/tests/unit/api/test_api_get_image.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -import shopdb.exceptions as exc -from shopdb.api import app +import shop_db2.exceptions as exc +from shop_db2.api import app from tests.base_api import BaseAPITestCase @@ -17,16 +17,14 @@ def test_get_existing_image(self): self.assertEqual(res.data, image_data) def test_get_non_existing_image(self): - """ - This test ensures that an exception is made when a non-existent image + """This test ensures that an exception is made when a non-existent image is requested. """ res = self.get("images/does_not_exist.png") self.assertException(res, exc.EntryNotFound) def test_get_image_empty_name(self): - """ - This test ensures that a standard image is returned if no file name + """This test ensures that a standard image is returned if no file name is requested. """ res = self.get("images/") diff --git a/tests/test_api_get_product.py b/tests/unit/api/test_api_get_product.py similarity index 96% rename from tests/test_api_get_product.py rename to tests/unit/api/test_api_get_product.py index c6c035f..b02e0f7 100644 --- a/tests/test_api_get_product.py +++ b/tests/unit/api/test_api_get_product.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, Tag +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, Tag from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_get_product_pricehistory.py b/tests/unit/api/test_api_get_product_pricehistory.py similarity index 84% rename from tests/test_api_get_product_pricehistory.py rename to tests/unit/api/test_api_get_product_pricehistory.py index 14179ef..c73e00d 100644 --- a/tests/test_api_get_product_pricehistory.py +++ b/tests/unit/api/test_api_get_product_pricehistory.py @@ -6,9 +6,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, ProductPrice +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, ProductPrice from tests.base_api import BaseAPITestCase @@ -24,16 +24,12 @@ def insert_pricehistory(dates=None): timestamps = [datetime.now()] * 4 for i in range(4): - p = ProductPrice( - price=prices[i], product_id=1, admin_id=1, timestamp=timestamps[i] - ) + p = ProductPrice(price=prices[i], product_id=1, admin_id=1, timestamp=timestamps[i]) db.session.add(p) db.session.commit() def test_authorization(self): - """ - This route should only be available for administrators. - """ + """This route should only be available for administrators.""" res = self.get(url="/products/1/pricehistory") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UnauthorizedAccess) @@ -42,16 +38,12 @@ def test_authorization(self): self.assertException(res, exc.UnauthorizedAccess) def test_get_pricehistory_non_existing_product(self): - """ - There should be an exception when the requested product does not exist. - """ + """There should be an exception when the requested product does not exist.""" res = self.get(url="/products/10/pricehistory", role="admin") self.assertException(res, exc.EntryNotFound) def test_get_pricehistory_invalid_start_or_end_date(self): - """ - There should be an exception when the start or end date are invalid. - """ + """There should be an exception when the start or end date are invalid.""" # Testing start date url = "/products/1/pricehistory?start_date=trololol" res = self.get(url=url, role="admin") @@ -68,17 +60,13 @@ def test_get_pricehistory_invalid_start_or_end_date(self): self.assertException(res, exc.WrongType) def test_get_pricehistory_end_before_start(self): - """ - There should be an exception when end date lies before the start date. - """ + """There should be an exception when end date lies before the start date.""" url = "/products/1/pricehistory?start_date=1000&end_date=900" res = self.get(url=url, role="admin") self.assertException(res, exc.InvalidData) def test_get_pricehistory_defining_only_start_date(self): - """ - Querying the pricehistory with only the start date given. - """ + """Querying the pricehistory with only the start date given.""" # Change the creation date of the product to 01.01.2019 dt = datetime.strptime("01.01.2019", "%d.%m.%Y") Product.query.filter_by(id=1).first().creation_date = dt @@ -97,9 +85,7 @@ def test_get_pricehistory_defining_only_start_date(self): self.assertEqual(len(pricehistory), 3) def test_get_pricehistory_defining_only_end_date(self): - """ - Querying the pricehistory with only the end date given. - """ + """Querying the pricehistory with only the end date given.""" # Change the creation date of the product to 01.01.2019 dt = datetime.strptime("01.01.2019", "%d.%m.%Y") Product.query.filter_by(id=1).first().creation_date = dt @@ -119,9 +105,7 @@ def test_get_pricehistory_defining_only_end_date(self): self.assertEqual(len(pricehistory), 2) def test_get_pricehistory_defining_start_and_end_date(self): - """ - Querying the pricehistory with start and end date given. - """ + """Querying the pricehistory with start and end date given.""" # Change the creation date of the product to 01.01.2019 dt = datetime.strptime("01.01.2019", "%d.%m.%Y") Product.query.filter_by(id=1).first().creation_date = dt diff --git a/tests/test_api_get_purchase.py b/tests/unit/api/test_api_get_purchase.py similarity index 96% rename from tests/test_api_get_purchase.py rename to tests/unit/api/test_api_get_purchase.py index d87d156..ff1da97 100644 --- a/tests/test_api_get_purchase.py +++ b/tests/unit/api/test_api_get_purchase.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_get_rank.py b/tests/unit/api/test_api_get_rank.py similarity index 96% rename from tests/test_api_get_rank.py rename to tests/unit/api/test_api_get_rank.py index 8579a29..8ed6a16 100644 --- a/tests/test_api_get_rank.py +++ b/tests/unit/api/test_api_get_rank.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_get_replenismencollection.py b/tests/unit/api/test_api_get_replenismencollection.py similarity index 93% rename from tests/test_api_get_replenismencollection.py rename to tests/unit/api/test_api_get_replenismencollection.py index e879129..cb075fc 100644 --- a/tests/test_api_get_replenismencollection.py +++ b/tests/unit/api/test_api_get_replenismencollection.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase @@ -45,8 +45,7 @@ def test_get_replenishmentcollection_as_user(self): self.assertException(res, exc.UnauthorizedAccess) def test_get_non_existing_replenishmentcollection(self): - """ - This test ensures that an exception is raised if the requested + """This test ensures that an exception is raised if the requested replenishmentcollection does not exist. """ self.insert_default_replenishmentcollections() diff --git a/tests/test_api_get_stocktaking_print_template.py b/tests/unit/api/test_api_get_stocktaking_print_template.py similarity index 82% rename from tests/test_api_get_stocktaking_print_template.py rename to tests/unit/api/test_api_get_stocktaking_print_template.py index 9f029f5..1a0abaa 100644 --- a/tests/test_api_get_stocktaking_print_template.py +++ b/tests/unit/api/test_api_get_stocktaking_print_template.py @@ -2,17 +2,16 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product + +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product from tests.base_api import BaseAPITestCase class GetStocktakingPrintTemplateAPITestCase(BaseAPITestCase): def test_get_stocktaking_template_file_type(self): - """ - This test verifies that the file format returned by the API is correct. - """ + """This test verifies that the file format returned by the API is correct.""" # Skip this test if pdfkit is not available try: import pdfkit @@ -25,8 +24,7 @@ def test_get_stocktaking_template_file_type(self): self.assertTrue(str(res.data).startswith("b'%PDF-")) def test_get_stocktaking_template_file_no_products(self): - """ - This test verifies that the correct exception is made when there are no + """This test verifies that the correct exception is made when there are no products available for stocktaking. """ # Skip this test if pdfkit is not available diff --git a/tests/test_api_get_stocktakingcollection.py b/tests/unit/api/test_api_get_stocktakingcollection.py similarity index 93% rename from tests/test_api_get_stocktakingcollection.py rename to tests/unit/api/test_api_get_stocktakingcollection.py index 46d8726..e527e08 100644 --- a/tests/test_api_get_stocktakingcollection.py +++ b/tests/unit/api/test_api_get_stocktakingcollection.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase @@ -36,8 +36,7 @@ def test_get_stocktakingcollection_as_user(self): self.assertException(res, exc.UnauthorizedAccess) def test_get_non_existing_stocktakingcollection(self): - """ - This test ensures that an exception is raised if the requested + """This test ensures that an exception is raised if the requested stocktakingcollection does not exist. """ self.insert_default_stocktakingcollections() diff --git a/tests/test_api_get_tag.py b/tests/unit/api/test_api_get_tag.py similarity index 95% rename from tests/test_api_get_tag.py rename to tests/unit/api/test_api_get_tag.py index c009139..c6885de 100644 --- a/tests/test_api_get_tag.py +++ b/tests/unit/api/test_api_get_tag.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_get_user.py b/tests/unit/api/test_api_get_user.py similarity index 90% rename from tests/test_api_get_user.py rename to tests/unit/api/test_api_get_user.py index dd32a2b..edfe3ef 100644 --- a/tests/test_api_get_user.py +++ b/tests/unit/api/test_api_get_user.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import User from tests.base import user_data from tests.base_api import BaseAPITestCase @@ -38,9 +38,7 @@ def test_get_non_verified_user(self): self.assertException(res, exc.UserIsNotVerified) def test_get_user_inactive_user(self): - """ - Getting an inactive user should raise an error. - """ + """Getting an inactive user should raise an error.""" User.query.filter_by(id=3).first().set_rank_id(4, 1) db.session.commit() res = self.get(url="/users/3") diff --git a/tests/test_api_get_user_deposits.py b/tests/unit/api/test_api_get_user_deposits.py similarity index 82% rename from tests/test_api_get_user_deposits.py rename to tests/unit/api/test_api_get_user_deposits.py index 2da4b00..adfb873 100644 --- a/tests/test_api_get_user_deposits.py +++ b/tests/unit/api/test_api_get_user_deposits.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Deposit, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Deposit, User from tests.base_api import BaseAPITestCase @@ -35,8 +35,7 @@ def test_get_user_deposit(self): assert x in i def test_get_user_deposits_no_insert(self): - """ - This test ensures that an empty list will be returned for a user's + """This test ensures that an empty list will be returned for a user's deposits if none have yet been entered for him. """ res = self.get(url="/users/2/deposits") @@ -45,25 +44,19 @@ def test_get_user_deposits_no_insert(self): self.assertEqual(deposits, []) def test_get_deposit_non_existing_user(self): - """ - Getting the deposits from a non existing user should raise an error. - """ + """Getting the deposits from a non existing user should raise an error.""" res = self.get(url="/users/6/deposits") self.assertEqual(res.status_code, 401) self.assertException(res, exc.EntryNotFound) def test_get_deposit_non_verified_user(self): - """ - Getting the deposits from a non verified user should raise an error. - """ + """Getting the deposits from a non verified user should raise an error.""" res = self.get(url="/users/4/deposits") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UserIsNotVerified) def test_get_user_deposits_inactive_user(self): - """ - Getting the deposits from an inactive user should raise an error. - """ + """Getting the deposits from an inactive user should raise an error.""" User.query.filter_by(id=3).first().set_rank_id(4, 1) db.session.commit() res = self.get(url="/users/3/deposits") diff --git a/tests/test_api_get_user_favorites.py b/tests/unit/api/test_api_get_user_favorites.py similarity index 80% rename from tests/test_api_get_user_favorites.py rename to tests/unit/api/test_api_get_user_favorites.py index 9295ce5..19f36d5 100644 --- a/tests/test_api_get_user_favorites.py +++ b/tests/unit/api/test_api_get_user_favorites.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, Purchase, User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, Purchase, User from tests.base_api import BaseAPITestCase @@ -32,9 +32,7 @@ def _insert_purchases(): db.session.commit() def test_get_user_favorites(self): - """ - This test ensures that the user's favorites are generated reliably. - """ + """This test ensures that the user's favorites are generated reliably.""" self._insert_purchases() res = self.get(url="/users/1/favorites") favorites = json.loads(res.data) @@ -42,8 +40,7 @@ def test_get_user_favorites(self): self.assertEqual(favorites, [3, 2, 1, 4]) def test_get_user_favorites_inactive_product(self): - """ - This test ensures that inactive products are not included in the + """This test ensures that inactive products are not included in the favorites. """ self._insert_purchases() @@ -58,8 +55,7 @@ def test_get_user_favorites_inactive_product(self): self.assertEqual(favorites, [3, 2, 4]) def test_get_user_favorites_no_purchase(self): - """ - This test ensures that an empty list is displayed for the user's + """This test ensures that an empty list is displayed for the user's favorites if no purchases have yet been made. """ res = self.get(url="/users/1/favorites") @@ -68,25 +64,19 @@ def test_get_user_favorites_no_purchase(self): self.assertEqual(favorites, []) def test_get_user_favorites_non_existing_user(self): - """ - Getting the favorites from a non existing user should raise an error. - """ + """Getting the favorites from a non existing user should raise an error.""" res = self.get(url="/users/6/favorites") self.assertEqual(res.status_code, 401) self.assertException(res, exc.EntryNotFound) def test_get_user_favorites_non_verified_user(self): - """ - Getting the favorites from a non verified user should raise an error. - """ + """Getting the favorites from a non verified user should raise an error.""" res = self.get(url="/users/4/favorites") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UserIsNotVerified) def test_get_user_favorites_inactive_user(self): - """ - Getting the favorites from an inactive user should raise an error. - """ + """Getting the favorites from an inactive user should raise an error.""" User.query.filter_by(id=3).first().set_rank_id(4, 1) db.session.commit() res = self.get(url="/users/3/favorites") diff --git a/tests/test_api_get_user_purchases.py b/tests/unit/api/test_api_get_user_purchases.py similarity index 79% rename from tests/test_api_get_user_purchases.py rename to tests/unit/api/test_api_get_user_purchases.py index 4208a76..1f6a7b6 100644 --- a/tests/test_api_get_user_purchases.py +++ b/tests/unit/api/test_api_get_user_purchases.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import User from tests.base_api import BaseAPITestCase @@ -31,16 +31,13 @@ def test_get_user_purchases(self): assert x in i def test_get_user_purchases_non_existing_user(self): - """ - This test ensures that an exception is made if the user does not exist. - """ + """This test ensures that an exception is made if the user does not exist.""" self.insert_default_purchases() res = self.get(url="/users/6/purchases") self.assertException(res, exc.EntryNotFound) def test_get_user_purchases_non_verified_user(self): - """ - This test ensures that an exception is made if the user has not been + """This test ensures that an exception is made if the user has not been verified yet. """ self.insert_default_purchases() @@ -48,8 +45,7 @@ def test_get_user_purchases_non_verified_user(self): self.assertException(res, exc.UserIsNotVerified) def test_get_users_purchases_no_insert(self): - """ - This test ensures that an empty list is returned for a user's + """This test ensures that an empty list is returned for a user's purchases if he has not yet made any purchases. """ res = self.get(url="/users/2/purchases") @@ -58,9 +54,7 @@ def test_get_users_purchases_no_insert(self): self.assertEqual(purchases, []) def test_get_user_purchases_inactive_user(self): - """ - Getting the purchases from an inactive user should raise an error. - """ + """Getting the purchases from an inactive user should raise an error.""" User.query.filter_by(id=3).first().set_rank_id(4, 1) db.session.commit() res = self.get(url="/users/3/purchases") diff --git a/tests/test_api_list_backups.py b/tests/unit/api/test_api_list_backups.py similarity index 92% rename from tests/test_api_list_backups.py rename to tests/unit/api/test_api_list_backups.py index e19b521..c3218ae 100644 --- a/tests/test_api_list_backups.py +++ b/tests/unit/api/test_api_list_backups.py @@ -7,7 +7,7 @@ from flask import json from pyfakefs import fake_filesystem_unittest -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase @@ -22,8 +22,7 @@ def test_authorization(self): self.assertException(res, exc.UnauthorizedAccess) def test_list_backups_no_backups_existing(self): - """ - This test checks whether the return value of the backup route is + """This test checks whether the return value of the backup route is correct when there are no backups. """ res = self.get("/backups", role="admin") @@ -32,10 +31,7 @@ def test_list_backups_no_backups_existing(self): self.assertFalse(data["latest"]) def test_list_backups(self): - """ - This test checks whether all backups get listed properly. - """ - + """This test checks whether all backups get listed properly.""" # We are using a fake filesystem to not actually create the files. self.setUpPyfakefs() diff --git a/tests/test_api_list_deposits.py b/tests/unit/api/test_api_list_deposits.py similarity index 95% rename from tests/test_api_list_deposits.py rename to tests/unit/api/test_api_list_deposits.py index 3045841..9c0e308 100644 --- a/tests/test_api_list_deposits.py +++ b/tests/unit/api/test_api_list_deposits.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase @@ -29,7 +29,8 @@ def test_list_deposits_as_admin(self): def test_list_deposits_as_user(self): """Test for listing all deposits without token. This should not be - possible.""" + possible. + """ res = self.get(url="/deposits") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UnauthorizedAccess) diff --git a/tests/test_api_list_products.py b/tests/unit/api/test_api_list_products.py similarity index 93% rename from tests/test_api_list_products.py rename to tests/unit/api/test_api_list_products.py index fcf7019..a672228 100644 --- a/tests/test_api_list_products.py +++ b/tests/unit/api/test_api_list_products.py @@ -4,8 +4,8 @@ from flask import json -from shopdb.api import db -from shopdb.models import Product, Tag +from shop_db2.api import db +from shop_db2.models import Product, Tag from tests.base_api import BaseAPITestCase @@ -40,8 +40,7 @@ def test_list_products(self): self.assertFalse(products[3]["active"]) def test_list_products_with_products_which_are_not_for_sale(self): - """ - This test ensures that the product listing differs between administrators and "normal" users. + """This test ensures that the product listing differs between administrators and "normal" users. Only if an administrator makes the request, all products should be returned, otherwise only those that are for sale. :return: diff --git a/tests/test_api_list_purchases.py b/tests/unit/api/test_api_list_purchases.py similarity index 94% rename from tests/test_api_list_purchases.py rename to tests/unit/api/test_api_list_purchases.py index ce69dd8..25300d0 100644 --- a/tests/test_api_list_purchases.py +++ b/tests/unit/api/test_api_list_purchases.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Purchase +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Purchase from tests.base_api import BaseAPITestCase @@ -39,7 +39,8 @@ def test_list_purchases_as_admin(self): def test_list_purchases_as_user(self): """Test for listing all purchases without token. Revoked purchases - should not be listed.""" + should not be listed. + """ # Do 5 purchases self.insert_default_purchases() # Revoke the third purchase @@ -62,9 +63,7 @@ def test_list_purchases_as_user(self): assert all(x not in purchase for x in forbidden) def test_list_purchases_with_limit(self): - """ - Listing the purchases with a limit. - """ + """Listing the purchases with a limit.""" # Do 5 purchases self.insert_default_purchases() diff --git a/tests/test_api_list_ranks.py b/tests/unit/api/test_api_list_ranks.py similarity index 100% rename from tests/test_api_list_ranks.py rename to tests/unit/api/test_api_list_ranks.py diff --git a/tests/test_api_list_replenishmentcollections.py b/tests/unit/api/test_api_list_replenishmentcollections.py similarity index 96% rename from tests/test_api_list_replenishmentcollections.py rename to tests/unit/api/test_api_list_replenishmentcollections.py index dc31ea4..210da81 100644 --- a/tests/test_api_list_replenishmentcollections.py +++ b/tests/unit/api/test_api_list_replenishmentcollections.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_list_stocktakingcollections.py b/tests/unit/api/test_api_list_stocktakingcollections.py similarity index 96% rename from tests/test_api_list_stocktakingcollections.py rename to tests/unit/api/test_api_list_stocktakingcollections.py index 18e929a..3699fb6 100644 --- a/tests/test_api_list_stocktakingcollections.py +++ b/tests/unit/api/test_api_list_stocktakingcollections.py @@ -4,7 +4,7 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_list_tags.py b/tests/unit/api/test_api_list_tags.py similarity index 100% rename from tests/test_api_list_tags.py rename to tests/unit/api/test_api_list_tags.py diff --git a/tests/test_api_list_users.py b/tests/unit/api/test_api_list_users.py similarity index 96% rename from tests/test_api_list_users.py rename to tests/unit/api/test_api_list_users.py index 86467e9..329cc2f 100644 --- a/tests/test_api_list_users.py +++ b/tests/unit/api/test_api_list_users.py @@ -4,15 +4,14 @@ from flask import json -from shopdb.api import db -from shopdb.models import Rank, User +from shop_db2.api import db +from shop_db2.models import Rank, User from tests.base_api import BaseAPITestCase class ListUsersAPITestCase(BaseAPITestCase): def test_list_users_as_user_and_external(self): """Get a list of all users as user and as external.""" - # Set user 3 inactive rank = Rank.query.filter(Rank.active.is_(False)).first() User.query.filter_by(id=3).first().set_rank_id(rank.id, 1) @@ -47,7 +46,8 @@ def test_list_users_as_user_and_external(self): def test_list_users_with_token(self): """Get a list of all users as admin. It should contain more information than the list which gets returned without a token in the request - header.""" + header. + """ res = self.get(url="/users", role="admin") self.assertEqual(res.status_code, 200) users = json.loads(res.data) diff --git a/tests/test_api_login.py b/tests/unit/api/test_api_login.py similarity index 86% rename from tests/test_api_login.py rename to tests/unit/api/test_api_login.py index d0a841e..3262649 100644 --- a/tests/test_api_login.py +++ b/tests/unit/api/test_api_login.py @@ -5,9 +5,9 @@ import jwt from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import User from tests.base import user_data from tests.base_api import BaseAPITestCase @@ -15,7 +15,8 @@ class LoginAPITestCase(BaseAPITestCase): def test_login_user(self): """This test is designed to test the login of an existing user with - an id and password""" + an id and password + """ data = {"id": 1, "password": user_data[0]["password"]} res = self.post(url="/login", data=data) self.assertEqual(res.status_code, 200) @@ -30,7 +31,8 @@ def test_login_user(self): def test_login_non_verified_user(self): """If an authentication attempt is made by a non verified user, - the correct error message must be returned.""" + the correct error message must be returned. + """ # Create a new user. data = { "firstname": "John", @@ -47,9 +49,9 @@ def test_login_non_verified_user(self): self.assertException(res, exc.UserIsNotVerified) def test_login_as_inactive_user(self): + """If an authentication attempt is made by an inactive user, + the correct error message must be returned. """ - If an authentication attempt is made by an inactive user, - the correct error message must be returned.""" User.query.filter_by(id=1).first().set_rank_id(4, 1) db.session.commit() data = {"id": 1, "password": user_data[0]["password"]} @@ -58,7 +60,8 @@ def test_login_as_inactive_user(self): def test_login_missing_password(self): """If an authentication attempt is made without a password, - the correct error message must be returned.""" + the correct error message must be returned. + """ data = {"id": 1} res = self.post(url="/login", data=data) self.assertEqual(res.status_code, 401) @@ -68,7 +71,8 @@ def test_login_missing_password(self): def test_login_missing_id(self): """If an authentication attempt is made without an id, - the correct error message must be returned.""" + the correct error message must be returned. + """ data = {"password": user_data[0]["password"]} res = self.post(url="/login", data=data) self.assertEqual(res.status_code, 401) @@ -78,7 +82,8 @@ def test_login_missing_id(self): def test_login_wrong_id(self): """If an authentication attempt is made with a wrong id, - the correct error message must be returned.""" + the correct error message must be returned. + """ data = {"id": 42, "password": user_data[0]["password"]} res = self.post(url="/login", data=data) self.assertEqual(res.status_code, 401) @@ -88,8 +93,8 @@ def test_login_wrong_id(self): def test_login_user_without_password(self): """If an authentication attempt is made by a user who has not set - a password yet, the correct error message must be returned.""" - + a password yet, the correct error message must be returned. + """ data = {"id": 3, "password": "DontCare"} res = self.post(url="/login", data=data) self.assertEqual(res.status_code, 401) @@ -99,7 +104,8 @@ def test_login_user_without_password(self): def test_login_wrong_password(self): """If an authentication attempt is made with a password, - the correct error message must be returned.""" + the correct error message must be returned. + """ data = {"id": 1, "password": "my_super_wrong_password"} res = self.post(url="/login", data=data) self.assertEqual(res.status_code, 401) diff --git a/tests/test_api_misc.py b/tests/unit/api/test_api_misc.py similarity index 93% rename from tests/test_api_misc.py rename to tests/unit/api/test_api_misc.py index 79f0958..c645f26 100644 --- a/tests/test_api_misc.py +++ b/tests/unit/api/test_api_misc.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import app -from shopdb.models import Purchase +import shop_db2.exceptions as exc +from shop_db2.api import app +from shop_db2.models import Purchase from tests.base_api import BaseAPITestCase @@ -39,8 +39,7 @@ def test_method_not_allowed_exception(self): self.assertEqual(data["result"], "error") def test_maintenance_mode(self): - """ - This test checks the maintenance mode. + """This test checks the maintenance mode. If the application is in maintenance mode, all requests must be aborted and the appropriate exception raised. There must be no modification to diff --git a/tests/test_api_query_parameters.py b/tests/unit/api/test_api_query_parameters.py similarity index 81% rename from tests/test_api_query_parameters.py rename to tests/unit/api/test_api_query_parameters.py index c0a7e70..a5f0ccd 100644 --- a/tests/test_api_query_parameters.py +++ b/tests/unit/api/test_api_query_parameters.py @@ -4,15 +4,13 @@ from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base_api import BaseAPITestCase class QueryParametersAPITestCase(BaseAPITestCase): def test_query_parameters_ordering(self): - """ - Test query ordering - """ + """Test query ordering""" # List all users ordered by their id in descending order users = json.loads( self.get( @@ -39,15 +37,9 @@ def test_query_parameters_ordering(self): ) def test_query_parameters_filter(self): - """ - Test query filter with a single and multiple values - """ + """Test query filter with a single and multiple values""" # List all users filtered by the lastname 'Smith' - users = json.loads( - self.get( - "/users", role="admin", params={"filter": {"lastname": "Smith"}} - ).data - ) + users = json.loads(self.get("/users", role="admin", params={"filter": {"lastname": "Smith"}}).data) # There should be only 'Mary Smith' with the id 2 self.assertEqual(1, len(users)) self.assertEqual("Smith", users[0]["lastname"]) @@ -55,11 +47,7 @@ def test_query_parameters_filter(self): self.assertEqual(2, users[0]["id"]) # List by boolean values - users = json.loads( - self.get( - "/users", role="admin", params={"filter": {"is_verified": False}} - ).data - ) + users = json.loads(self.get("/users", role="admin", params={"filter": {"is_verified": False}}).data) self.assertEqual(1, len(users)) self.assertEqual("Lee", users[0]["lastname"]) self.assertEqual("Daniel", users[0]["firstname"]) @@ -82,9 +70,7 @@ def test_query_parameters_filter(self): self.assertEqual(4, users[1]["id"]) # List all users with the rank_id 1 - users = json.loads( - self.get("/users", role="admin", params={"filter": {"rank_id": 1}}).data - ) + users = json.loads(self.get("/users", role="admin", params={"filter": {"rank_id": 1}}).data) # There should only be Bryce Jones self.assertEqual(2, len(users)) self.assertEqual("Jones", users[0]["lastname"]) @@ -101,9 +87,7 @@ def test_query_parameters_filter(self): self.assertEqual("Mary", users[0]["firstname"]) # If we add another filter with ids, there should still be only Mary - params = { - "filter": {"lastname": ["Smith", "Lee"], "firstname": "Mary", "id": [2, 3]} - } + params = {"filter": {"lastname": ["Smith", "Lee"], "firstname": "Mary", "id": [2, 3]}} users = json.loads(self.get("/users", role="admin", params=params).data) # There should only be Mary Smith self.assertEqual(1, len(users)) @@ -111,17 +95,13 @@ def test_query_parameters_filter(self): self.assertEqual("Mary", users[0]["firstname"]) # By removing her id from the filter, no user should be found - params = { - "filter": {"lastname": ["Smith", "Lee"], "firstname": "Mary", "id": 3} - } + params = {"filter": {"lastname": ["Smith", "Lee"], "firstname": "Mary", "id": 3}} users = json.loads(self.get("/users", role="admin", params=params).data) # There shouldn't be any results self.assertEqual(0, len(users)) def test_query_parameters_sorting(self): - """ - Test query sorting - """ + """Test query sorting""" # List all users and sort them by their firstname params = {"sort": {"field": "firstname", "order": "ASC"}} users = json.loads(self.get("/users", role="admin", params=params).data) @@ -135,47 +115,29 @@ def test_query_parameters_sorting(self): # List all users and sort them by their credit params = {"sort": {"field": "credit", "order": "ASC"}} users = json.loads(self.get("/users", role="admin", params=params).data) - self.assertEqual( - [-1800, -1100, -400, 0, 0], list(map(lambda x: x["credit"], users)) - ) + self.assertEqual([-1800, -1100, -400, 0, 0], list(map(lambda x: x["credit"], users))) def test_query_parameters_pagination(self): - """ - Test query pagination - """ + """Test query pagination""" # List all users with the pagination {'page': 1, 'perPage': 1} - users = json.loads( - self.get( - "/users", role="admin", params={"pagination": {"page": 1, "perPage": 1}} - ).data - ) + users = json.loads(self.get("/users", role="admin", params={"pagination": {"page": 1, "perPage": 1}}).data) # There should be exactly 1 user (default ordering is [id, ASC] so its the first user) self.assertEqual([1], list(map(lambda x: x["id"], users))) # List all users with the pagination {'page': 1, 'perPage': 3} - users = json.loads( - self.get( - "/users", role="admin", params={"pagination": {"page": 1, "perPage": 3}} - ).data - ) + users = json.loads(self.get("/users", role="admin", params={"pagination": {"page": 1, "perPage": 3}}).data) # There should be exactly 3 users self.assertEqual(3, len(users)) self.assertEqual([1, 2, 3], list(map(lambda x: x["id"], users))) # List all users with the pagination {'page': 2, 'perPage': 2} - users = json.loads( - self.get( - "/users", role="admin", params={"pagination": {"page": 2, "perPage": 2}} - ).data - ) + users = json.loads(self.get("/users", role="admin", params={"pagination": {"page": 2, "perPage": 2}}).data) # There should be exactly 2 users self.assertEqual(2, len(users)) self.assertEqual([3, 4], list(map(lambda x: x["id"], users))) def test_invalid_query_parameters(self): - """ - This test ensures that only valid query parameters are accepted by the API - """ + """This test ensures that only valid query parameters are accepted by the API""" param_list = [ # Invalid sort column foo { diff --git a/tests/test_api_toggle_maintenance.py b/tests/unit/api/test_api_toggle_maintenance.py similarity index 86% rename from tests/test_api_toggle_maintenance.py rename to tests/unit/api/test_api_toggle_maintenance.py index 233aa41..0c2eb24 100644 --- a/tests/test_api_toggle_maintenance.py +++ b/tests/unit/api/test_api_toggle_maintenance.py @@ -8,17 +8,17 @@ from flask import json -import shopdb.exceptions as exc -from configuration import PATH -from shopdb.api import app +import shop_db2.exceptions as exc +from shop_db2.api import app from tests.base_api import BaseAPITestCase +from configuration import PATH # isort: skip + class ToggleMaintenanceAPITestCase(BaseAPITestCase): @staticmethod def get_config_file_maintenance_mode() -> bool: - """ - This helper function reads the config file content and returns the current maintenance state. + """This helper function reads the config file content and returns the current maintenance state. :return: The current maintenance state in the config file """ @@ -43,9 +43,7 @@ def test_toggle_maintenance_mode_authorization(self): self.assertException(res, exc.DataIsMissing) def test_turn_on_maintenance_mode(self): - """ - This test ensures that the maintenance mode can be activated. - """ + """This test ensures that the maintenance mode can be activated.""" # Check the maintenance state self.assertFalse(app.config["MAINTENANCE"]) self.assertFalse(self.get_config_file_maintenance_mode()) @@ -65,9 +63,7 @@ def test_turn_on_maintenance_mode(self): self.post(url="/maintenance", data={"state": False}, role="admin") def test_turn_off_maintenance_mode(self): - """ - This test ensures that the maintenance mode can be deactivated. - """ + """This test ensures that the maintenance mode can be deactivated.""" # Set the maintenance state to "True" self.post(url="/maintenance", data={"state": True}, role="admin") @@ -87,17 +83,14 @@ def test_turn_off_maintenance_mode(self): self.assertEqual(data["message"], "Turned maintenance mode off.") def test_toggle_maintenance_mode_without_change(self): - """ - This test ensures that an exception is raised if the maintenance mode + """This test ensures that an exception is raised if the maintenance mode would not be changed by the request. """ res = self.post(url="/maintenance", data={"state": False}, role="admin") self.assertException(res, exc.NothingHasChanged) def test_user_interaction_in_maintenance_mode(self): - """ - This test ensures that an exception is raised if a user (no administrator) does any request. - """ + """This test ensures that an exception is raised if a user (no administrator) does any request.""" # Set the maintenance state to "False" self.post(url="/maintenance", data={"state": True}, role="admin") diff --git a/tests/test_api_token.py b/tests/unit/api/test_api_token.py similarity index 97% rename from tests/test_api_token.py rename to tests/unit/api/test_api_token.py index f8f450b..959c1f5 100644 --- a/tests/test_api_token.py +++ b/tests/unit/api/test_api_token.py @@ -7,7 +7,7 @@ import jwt from flask import json -import shopdb.exceptions as exc +import shop_db2.exceptions as exc from tests.base import user_data from tests.base_api import BaseAPITestCase @@ -58,7 +58,8 @@ def test_token_expired(self): def test_token_missing_user(self): """Each token contains a user dictionary. If it is missing, an error - should be raised.""" + should be raised. + """ data = {"id": 1, "password": user_data[0]["password"]} res = self.post(url="/login", data=data) token = json.loads(res.data)["token"] diff --git a/tests/test_api_update_deposit.py b/tests/unit/api/test_api_update_deposit.py similarity index 98% rename from tests/test_api_update_deposit.py rename to tests/unit/api/test_api_update_deposit.py index 98ad9d1..3bf1b67 100644 --- a/tests/test_api_update_deposit.py +++ b/tests/unit/api/test_api_update_deposit.py @@ -4,8 +4,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import Deposit +import shop_db2.exceptions as exc +from shop_db2.models import Deposit from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_update_product.py b/tests/unit/api/test_api_update_product.py similarity index 96% rename from tests/test_api_update_product.py rename to tests/unit/api/test_api_update_product.py index dc7f9cd..376458e 100644 --- a/tests/test_api_update_product.py +++ b/tests/unit/api/test_api_update_product.py @@ -7,9 +7,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import app, db -from shopdb.models import Product, ProductPrice +import shop_db2.exceptions as exc +from shop_db2.api import app, db +from shop_db2.models import Product, ProductPrice from tests.base_api import BaseAPITestCase @@ -135,8 +135,7 @@ def test_update_product_image(self): os.remove(filepath) def test_update_barcode_with_existing_barcode(self): - """ - It should not be possible to assign a barcode to a product which has + """It should not be possible to assign a barcode to a product which has been assigned to another product. """ Product.query.filter_by(id=1).first().barcode = "123456" diff --git a/tests/test_api_update_purchase.py b/tests/unit/api/test_api_update_purchase.py similarity index 94% rename from tests/test_api_update_purchase.py rename to tests/unit/api/test_api_update_purchase.py index 0dbd547..b5766f8 100644 --- a/tests/test_api_update_purchase.py +++ b/tests/unit/api/test_api_update_purchase.py @@ -4,9 +4,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Product, Purchase +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Product, Purchase from tests.base_api import BaseAPITestCase @@ -41,7 +41,8 @@ def test_update_non_existing_purchase(self): def test_revoke_purchase_made_by_admin(self): """Purchase, which have been inserted from administrators can only - be revoked by an administrator.""" + be revoked by an administrator. + """ # Create purchase with admin privileges data = {"user_id": 2, "product_id": 1, "amount": 1} self.post(url="/purchases", data=data, role="admin") @@ -52,9 +53,7 @@ def test_revoke_purchase_made_by_admin(self): # Users are not allowed to revoke (or even update) this purchase for role in [None, "user"]: - res = self.put( - url=f"/purchases/{purchase.id}", data={"revoked": True}, role=role - ) + res = self.put(url=f"/purchases/{purchase.id}", data={"revoked": True}, role=role) self.assertException(res, exc.EntryNotRevocable) purchase = Purchase.query.order_by(Purchase.id.desc()).first() self.assertFalse(purchase.revoked) @@ -115,9 +114,7 @@ def test_update_purchase_revoked(self): self.assertTrue(Purchase.query.filter_by(id=1).first().revoked) def test_update_non_revocable_purchase_revoke(self): - """ - In case that the product is not revocable, an exception must be made. - """ + """In case that the product is not revocable, an exception must be made.""" # Make sure, that product 1 is not revocable. product = Product.query.filter_by(id=1).first() product.revocable = False diff --git a/tests/test_api_update_rank.py b/tests/unit/api/test_api_update_rank.py similarity index 93% rename from tests/test_api_update_rank.py rename to tests/unit/api/test_api_update_rank.py index de9e3b1..25d4812 100644 --- a/tests/test_api_update_rank.py +++ b/tests/unit/api/test_api_update_rank.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -import shopdb.exceptions as exc -from shopdb.models import Rank +import shop_db2.exceptions as exc +from shop_db2.models import Rank from tests.base_api import BaseAPITestCase @@ -53,17 +53,13 @@ def test_update_rank_name(self): self.assertEqual(Rank.query.filter_by(id=1).first().name, "Foo") def test_update_rank_is_system_user(self): - """ - Update the "is_system_user" field - """ + """Update the "is_system_user" field""" self.assertFalse(Rank.query.filter_by(id=1).first().is_system_user) self.put(url="/ranks/1", data={"is_system_user": True}, role="admin") self.assertTrue(Rank.query.filter_by(id=1).first().is_system_user) def test_update_rank_debt_limit(self): - """ - Update the "debt_limit" field - """ + """Update the "debt_limit" field""" self.assertEqual(0, Rank.query.filter_by(id=1).first().debt_limit) self.put(url="/ranks/1", data={"debt_limit": 100}, role="admin") self.assertEqual(100, Rank.query.filter_by(id=1).first().debt_limit) diff --git a/tests/test_api_update_replenishment.py b/tests/unit/api/test_api_update_replenishment.py similarity index 97% rename from tests/test_api_update_replenishment.py rename to tests/unit/api/test_api_update_replenishment.py index 043fc4a..afa25d1 100644 --- a/tests/test_api_update_replenishment.py +++ b/tests/unit/api/test_api_update_replenishment.py @@ -4,8 +4,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import Replenishment, ReplenishmentCollection +import shop_db2.exceptions as exc +from shop_db2.models import Replenishment, ReplenishmentCollection from tests.base_api import BaseAPITestCase @@ -33,8 +33,7 @@ def test_update_replenishment_no_changes(self): self.assertException(res, exc.NothingHasChanged) def test_update_replenishment_no_changes_II(self): - """ - Trying to un-revoke a non revoked replenishment should raise an + """Trying to un-revoke a non revoked replenishment should raise an exception. """ self.insert_default_replenishmentcollections() @@ -134,7 +133,8 @@ def test_update_replenishment_revoke_all(self): def test_update_replenishment_rerevoke_replcoll(self): """Re-revoking a replenishment after all replenishments have been - revoked should re-revoke the corresponding replenishmentcollection""" + revoked should re-revoke the corresponding replenishmentcollection + """ self.test_update_replenishment_revoke_all() data = {"revoked": False} res = self.put(url="/replenishments/1", data=data, role="admin") diff --git a/tests/test_api_update_replenishmentcollection.py b/tests/unit/api/test_api_update_replenishmentcollection.py similarity index 81% rename from tests/test_api_update_replenishmentcollection.py rename to tests/unit/api/test_api_update_replenishmentcollection.py index a312183..ce695cf 100644 --- a/tests/test_api_update_replenishmentcollection.py +++ b/tests/unit/api/test_api_update_replenishmentcollection.py @@ -6,8 +6,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import ReplenishmentCollection +import shop_db2.exceptions as exc +from shop_db2.models import ReplenishmentCollection from tests.base_api import BaseAPITestCase @@ -15,9 +15,7 @@ class UpdateReplenishmentCollectionsAPITestCase(BaseAPITestCase): def test_revoke_replenishmentcollection(self): """Revoke a replenishmentcollection""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/1", data={"revoked": True}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) data = json.loads(res.data) self.assertTrue("message" in data) @@ -31,17 +29,11 @@ def test_revoke_replenishmentcollection(self): def test_revoke_replenishmentcollection_multiple_times(self): """Revoke a replenishmentcollection multiple times""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/1", data={"revoked": True}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) - res = self.put( - url="/replenishmentcollections/1", data={"revoked": False}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": False}, role="admin") self.assertEqual(res.status_code, 201) - res = self.put( - url="/replenishmentcollections/1", data={"revoked": True}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) replcoll = ReplenishmentCollection.query.filter_by(id=1).first() self.assertEqual(replcoll.revoked, True) @@ -54,9 +46,7 @@ def test_revoke_replenishmentcollection_multiple_times(self): def test_update_replenishmentcollection_comment(self): """Update the comment of a replenishmentcollection""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/1", data={"comment": "FooBar"}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"comment": "FooBar"}, role="admin") self.assertEqual(res.status_code, 201) data = json.loads(res.data) self.assertTrue("message" in data) @@ -97,27 +87,21 @@ def test_update_replenishmentcollection_invalid_timestamp(self): def test_revoke_replenishmentcollection_as_user(self): """Revoking a replenishmentcollection as user should be forbidden""" - res = self.put( - url="/replenishmentcollections/1", data={"revoked": True}, role="user" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": True}, role="user") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UnauthorizedAccess) def test_update_replenishmentcollection_no_changes(self): """Revoking a replenishmentcollection with no changes""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/1", data={"revoked": False}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": False}, role="admin") self.assertEqual(res.status_code, 200) self.assertException(res, exc.NothingHasChanged) def test_update_non_existing_replenishmentcollection(self): """Revoking a replenishmentcollection that doesnt exist""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/4", data={"revoked": True}, role="admin" - ) + res = self.put(url="/replenishmentcollections/4", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.EntryNotFound) @@ -135,18 +119,14 @@ def test_update_replenishmentcollection_forbidden_field(self): def test_update_replenishmentcollection_unknown_field(self): """Update non existing fields of a replenishmentcollection""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/1", data={"Nonsense": ""}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"Nonsense": ""}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UnknownField) def test_update_replenishmentcollection_wrong_type(self): """Update fields of a replenishmentcollection with wrong types""" self.insert_default_replenishmentcollections() - res = self.put( - url="/replenishmentcollections/1", data={"revoked": "yes"}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": "yes"}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.WrongType) @@ -159,7 +139,8 @@ def test_update_replenishmentcollection_with_no_data(self): def test_update_replenishmentcollection_revoke_error(self): """Trying to rerevoke a replenishmentcollection which only has revoked - replenishments should raise an error""" + replenishments should raise an error + """ self.insert_default_replenishmentcollections() # revoke the corresponding replenishments res = self.put(url="/replenishments/1", data={"revoked": True}, role="admin") @@ -167,8 +148,6 @@ def test_update_replenishmentcollection_revoke_error(self): res = self.put(url="/replenishments/2", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) # actual test - res = self.put( - url="/replenishmentcollections/1", data={"revoked": False}, role="admin" - ) + res = self.put(url="/replenishmentcollections/1", data={"revoked": False}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.EntryNotRevocable) diff --git a/tests/test_api_update_stocktaking.py b/tests/unit/api/test_api_update_stocktaking.py similarity index 97% rename from tests/test_api_update_stocktaking.py rename to tests/unit/api/test_api_update_stocktaking.py index fb54765..4976a06 100644 --- a/tests/test_api_update_stocktaking.py +++ b/tests/unit/api/test_api_update_stocktaking.py @@ -4,8 +4,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import Stocktaking +import shop_db2.exceptions as exc +from shop_db2.models import Stocktaking from tests.base_api import BaseAPITestCase diff --git a/tests/test_api_update_stocktakingcollection.py b/tests/unit/api/test_api_update_stocktakingcollection.py similarity index 76% rename from tests/test_api_update_stocktakingcollection.py rename to tests/unit/api/test_api_update_stocktakingcollection.py index 6640a11..0e8e923 100644 --- a/tests/test_api_update_stocktakingcollection.py +++ b/tests/unit/api/test_api_update_stocktakingcollection.py @@ -4,8 +4,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import StocktakingCollection +import shop_db2.exceptions as exc +from shop_db2.models import StocktakingCollection from tests.base_api import BaseAPITestCase @@ -13,9 +13,7 @@ class UpdateStocktakingCollectionsAPITestCase(BaseAPITestCase): def test_revoke_stocktakingcollection(self): """Revoke a stocktakingcollection""" self.insert_default_stocktakingcollections() - res = self.put( - url="/stocktakingcollections/1", data={"revoked": True}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) data = json.loads(res.data) self.assertTrue("message" in data) @@ -29,17 +27,11 @@ def test_revoke_stocktakingcollection(self): def test_revoke_stocktakingcollection_multiple_times(self): """Revoke a stocktakingcollection multiple times""" self.insert_default_stocktakingcollections() - res = self.put( - url="/stocktakingcollections/1", data={"revoked": True}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) - res = self.put( - url="/stocktakingcollections/1", data={"revoked": False}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": False}, role="admin") self.assertEqual(res.status_code, 201) - res = self.put( - url="/stocktakingcollections/1", data={"revoked": True}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 201) collection = StocktakingCollection.query.filter_by(id=1).first() self.assertEqual(collection.revoked, True) @@ -51,27 +43,21 @@ def test_revoke_stocktakingcollection_multiple_times(self): def test_revoke_stocktakingcollection_as_user(self): """Revoking a stocktakingcollection as user should be forbidden""" - res = self.put( - url="/stocktakingcollections/1", data={"revoked": True}, role="user" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": True}, role="user") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UnauthorizedAccess) def test_update_stocktakingcollection_no_changes(self): """Revoking a stocktakingcollection with no changes""" self.insert_default_stocktakingcollections() - res = self.put( - url="/stocktakingcollections/1", data={"revoked": False}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": False}, role="admin") self.assertEqual(res.status_code, 200) self.assertException(res, exc.NothingHasChanged) def test_update_non_existing_stocktakingcollection(self): """Revoking a stocktakingcollection that doesnt exist""" self.insert_default_stocktakingcollections() - res = self.put( - url="/stocktakingcollections/4", data={"revoked": True}, role="admin" - ) + res = self.put(url="/stocktakingcollections/4", data={"revoked": True}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.EntryNotFound) @@ -89,18 +75,14 @@ def test_update_stocktakingcollection_forbidden_field(self): def test_update_stocktakingcollection_unknown_field(self): """Update non existing fields of a stocktakingcollection""" self.insert_default_stocktakingcollections() - res = self.put( - url="/stocktakingcollections/1", data={"Nonsense": ""}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"Nonsense": ""}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.UnknownField) def test_update_stocktakingcollection_wrong_type(self): """Update fields of a stocktakingcollection with wrong types""" self.insert_default_stocktakingcollections() - res = self.put( - url="/stocktakingcollections/1", data={"revoked": "yes"}, role="admin" - ) + res = self.put(url="/stocktakingcollections/1", data={"revoked": "yes"}, role="admin") self.assertEqual(res.status_code, 401) self.assertException(res, exc.WrongType) diff --git a/tests/test_api_update_tag.py b/tests/unit/api/test_api_update_tag.py similarity index 95% rename from tests/test_api_update_tag.py rename to tests/unit/api/test_api_update_tag.py index 4ecdfb2..20c8602 100644 --- a/tests/test_api_update_tag.py +++ b/tests/unit/api/test_api_update_tag.py @@ -4,8 +4,8 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.models import Product, Tag +import shop_db2.exceptions as exc +from shop_db2.models import Product, Tag from tests.base_api import BaseAPITestCase @@ -68,9 +68,7 @@ def test_update_tag_name(self): self.assertEqual(Tag.query.filter_by(id=1).first().name, "Foo") def test_update_tag_is_for_sale(self): - """ - Update the "is_for_sale" field - """ + """Update the "is_for_sale" field""" self.assertTrue(Tag.query.filter_by(id=1).first().is_for_sale) self.put(url="/tags/1", data={"is_for_sale": False}, role="admin") self.assertFalse(Tag.query.filter_by(id=1).first().is_for_sale) diff --git a/tests/test_api_update_user.py b/tests/unit/api/test_api_update_user.py similarity index 96% rename from tests/test_api_update_user.py rename to tests/unit/api/test_api_update_user.py index 185b5e2..3a81c53 100644 --- a/tests/test_api_update_user.py +++ b/tests/unit/api/test_api_update_user.py @@ -7,9 +7,9 @@ from flask import json -import shopdb.exceptions as exc -from shopdb.api import app -from shopdb.models import User +import shop_db2.exceptions as exc +from shop_db2.api import app +from shop_db2.models import User from tests.base import user_data from tests.base_api import BaseAPITestCase @@ -28,9 +28,7 @@ def test_update_authorization(self): self.assertException(res, exc.NothingHasChanged) def test_update_user_non_verified(self): - """ - This test ensures that a non verified user can only be updated if the required data is given - """ + """This test ensures that a non verified user can only be updated if the required data is given""" user = User.query.filter_by(id=4).first() self.assertEqual(user.firstname, user_data[3]["firstname"]) data = {"firstname": "Bob", "rank_id": 2} @@ -55,7 +53,8 @@ def test_promote_user_to_admin(self): def test_promote_user_to_admin_twice(self): """When a user gets promoted to an admin twice, nothing - should change.""" + should change. + """ self.assertTrue(User.query.filter_by(id=1).first().is_admin) data = {"is_admin": True} res = self.put(url="/users/1", data=data, role="admin") @@ -84,7 +83,8 @@ def test_make_contender_member(self): def test_make_contender_member_twice(self): """Update a contender to a member twice should raise - NothingHasChanged""" + NothingHasChanged + """ self.assertEqual(User.query.filter_by(id=3).first().rank_id, 1) data = {"rank_id": 1} res = self.put(url="/users/3", data=data, role="admin") @@ -133,8 +133,7 @@ def test_update_non_existing_user(self): self.assertException(res, exc.EntryNotFound) def test_update_user_password_too_short(self): - """ - This test ensures that an exception is made if the user's password is + """This test ensures that an exception is made if the user's password is too short. """ data = {"password": "short", "password_repeat": "short"} diff --git a/tests/unit/helpers/__init__.py b/tests/unit/helpers/__init__.py new file mode 100644 index 0000000..18e9ba1 --- /dev/null +++ b/tests/unit/helpers/__init__.py @@ -0,0 +1 @@ +"""Contains all unit tests for the models.""" diff --git a/tests/test_helpers_products.py b/tests/unit/helpers/test_helpers_products.py similarity index 88% rename from tests/test_helpers_products.py rename to tests/unit/helpers/test_helpers_products.py index 8f42088..875c25f 100644 --- a/tests/test_helpers_products.py +++ b/tests/unit/helpers/test_helpers_products.py @@ -4,27 +4,23 @@ from datetime import datetime -import shopdb.exceptions as exc -import shopdb.helpers.products as product_helpers -from shopdb.api import db -from shopdb.models import (Product, ProductPrice, Purchase, - StocktakingCollection) +import shop_db2.exceptions as exc +import shop_db2.helpers.products as product_helpers +from shop_db2.api import db +from shop_db2.models import Product, ProductPrice, Purchase, StocktakingCollection from tests.base_api import BaseAPITestCase class TestHelpersProductsTestCase(BaseAPITestCase): def test_get_product_mean_price_in_range(self): - """ - This test checks whether the average weighted price of a product is + """This test checks whether the average weighted price of a product is correctly determined within a given period of time. """ # Manipulate the timestamp of the first product price. t = datetime.strptime("2017-01-01 09:00:00", "%Y-%m-%d %H:%M:%S") ProductPrice.query.filter_by(product_id=1).first().timestamp = t db.session.commit() - self.assertEqual( - ProductPrice.query.filter_by(product_id=1).first().timestamp, t - ) + self.assertEqual(ProductPrice.query.filter_by(product_id=1).first().timestamp, t) # Insert some product price changes. t1 = datetime.strptime("2017-02-01 09:00:00", "%Y-%m-%d %H:%M:%S") @@ -88,15 +84,12 @@ def test_get_product_mean_price_in_range(self): self.assertEqual(mean, 145) def test_get_product_mean_price_in_range_invalid_params(self): - """ - This test ensures that the helper function raises the corresponding + """This test ensures that the helper function raises the corresponding exceptions for invalid parameters. """ # Invalid dates with self.assertRaises(exc.InvalidData): - product_helpers._get_product_mean_price_in_time_range( - 1, "01.01.2018", "02.01.2018" - ) + product_helpers._get_product_mean_price_in_time_range(1, "01.01.2018", "02.01.2018") # Invalid product start = datetime.strptime("2017-01-31 09:00:00", "%Y-%m-%d %H:%M:%S") @@ -111,9 +104,7 @@ def test_get_product_mean_price_in_range_invalid_params(self): product_helpers._get_product_mean_price_in_time_range(1, start, end) def test_get_theoretical_stock_of_product(self): - """ - This test checks the "get_theoretical_stock_of_product" helper function. - """ + """This test checks the "get_theoretical_stock_of_product" helper function.""" # Insert the default stocktaking collections. self.insert_default_stocktakingcollections() diff --git a/tests/test_helpers_purchases.py b/tests/unit/helpers/test_helpers_purchases.py similarity index 71% rename from tests/test_helpers_purchases.py rename to tests/unit/helpers/test_helpers_purchases.py index bbc3a48..b4a6537 100644 --- a/tests/test_helpers_purchases.py +++ b/tests/unit/helpers/test_helpers_purchases.py @@ -4,24 +4,20 @@ from datetime import datetime -import shopdb.helpers.purchases as purchase_helpers -from shopdb.api import db -from shopdb.models import Purchase +import shop_db2.helpers.purchases as purchase_helpers +from shop_db2.api import db +from shop_db2.models import Purchase from tests.base_api import BaseAPITestCase class TestHelpersPurchasesTestCase(BaseAPITestCase): def test_get_purchase_amount_in_interval(self): - """ - This test checks the "get_purchase_amount_in_interval" helper function. - """ + """This test checks the "get_purchase_amount_in_interval" helper function.""" # Insert some purchases t1 = datetime.strptime("2018-02-01 09:00:00", "%Y-%m-%d %H:%M:%S") db.session.add(Purchase(user_id=1, product_id=1, amount=1, timestamp=t1)) t2 = datetime.strptime("2018-02-02 09:00:00", "%Y-%m-%d %H:%M:%S") - db.session.add( - Purchase(user_id=1, product_id=1, amount=5, timestamp=t2, revoked=True) - ) + db.session.add(Purchase(user_id=1, product_id=1, amount=5, timestamp=t2, revoked=True)) t3 = datetime.strptime("2018-02-03 09:00:00", "%Y-%m-%d %H:%M:%S") db.session.add(Purchase(user_id=2, product_id=1, amount=8, timestamp=t3)) t4 = datetime.strptime("2018-02-04 09:00:00", "%Y-%m-%d %H:%M:%S") @@ -29,7 +25,5 @@ def test_get_purchase_amount_in_interval(self): db.session.commit() # Get purchases in interval (second is revoked!) - purchase_amount = purchase_helpers.get_purchase_amount_in_interval( - product_id=1, start=t1, end=t4 - ) + purchase_amount = purchase_helpers.get_purchase_amount_in_interval(product_id=1, start=t1, end=t4) self.assertEqual(9, purchase_amount) diff --git a/tests/test_helpers_stocktakings.py b/tests/unit/helpers/test_helpers_stocktakings.py similarity index 95% rename from tests/test_helpers_stocktakings.py rename to tests/unit/helpers/test_helpers_stocktakings.py index c162745..2d021b8 100644 --- a/tests/test_helpers_stocktakings.py +++ b/tests/unit/helpers/test_helpers_stocktakings.py @@ -4,18 +4,15 @@ from datetime import datetime -import shopdb.helpers.stocktakings as stocktaking_helpers -from shopdb.api import db -from shopdb.models import (Product, ProductPrice, Purchase, - ReplenishmentCollection, Stocktaking, - StocktakingCollection) +import shop_db2.helpers.stocktakings as stocktaking_helpers +from shop_db2.api import db +from shop_db2.models import Product, ProductPrice, Purchase, ReplenishmentCollection, Stocktaking, StocktakingCollection from tests.base_api import BaseAPITestCase class TestHelpersStocktakingsTestCase(BaseAPITestCase): def test_balance_between_stocktakings_one_stocktaking(self): - """ - If only one stocktaking was made, no balance calculation can be done. + """If only one stocktaking was made, no balance calculation can be done. This is checked with this test. """ # Insert the initial stocktaking. @@ -38,11 +35,9 @@ def test_balance_between_stocktakings_one_stocktaking(self): self.assertEqual(result, None) def test_balance_between_stocktakings_two_stocktakings(self): - """ - This test checks whether the calculation of the balance works correctly + """This test checks whether the calculation of the balance works correctly between two stocktakings. """ - # Insert a purchase which lies before the first stocktaking. t = datetime.strptime("2017-01-01 09:00:00", "%Y-%m-%d %H:%M:%S") db.session.add(Purchase(user_id=1, product_id=1, amount=100, timestamp=t)) @@ -173,8 +168,7 @@ def test_balance_between_stocktakings_two_stocktakings(self): self.assertEqual(result["profit"], 0) def test_balance_between_stocktakings_multiple_stocktakings(self): - """ - This test checks whether the calculation of the balance works + """This test checks whether the calculation of the balance works correctly over several stocktakings. """ # Since the start of this test is the same as the previous one, it @@ -303,9 +297,7 @@ def test_balance_between_stocktakings_multiple_stocktakings(self): self.assertEqual(result["profit"], 0) def test_balance_between_stocktakings_forgotten_purchases_inactive_products(self): - """ - TODO - """ + """TODO:""" # Since the start of this test is the same as the previous one, it # can be run again to generate the required data. self.test_balance_between_stocktakings_multiple_stocktakings() @@ -378,8 +370,7 @@ def test_balance_between_stocktakings_forgotten_purchases_inactive_products(self self.assertEqual(result["profit"], 0) def test_balance_between_stocktakings_product_set_to_inactive(self): - """ - This test checks whether the calculation of the balance works + """This test checks whether the calculation of the balance works correctly if a product has been set to inactive since the first stocktaking. """ @@ -482,11 +473,9 @@ def test_balance_between_stocktakings_product_set_to_inactive(self): self.assertEqual(result["profit"], 0) def test_balance_between_stocktakings_product_creation(self): - """ - This test checks whether the calculation of the balance works + """This test checks whether the calculation of the balance works correctly if a new product has been added since the first stocktaking. """ - # Insert the first stocktaking db.session.add(StocktakingCollection(admin_id=1)) db.session.flush() @@ -594,8 +583,7 @@ def test_balance_between_stocktakings_product_creation(self): self.assertEqual(result["profit"], 0) def test_balance_between_stocktakings_new_product_and_set_inactive(self): - """ - If a product was created between the first and the last stocktaking, + """If a product was created between the first and the last stocktaking, which was then reset to inactive, the calculation of the balance must still be carried out correctly. This functionality is checked with this test. @@ -682,8 +670,7 @@ def test_balance_between_stocktakings_new_product_and_set_inactive(self): self.assertEqual(result["profit"], 0) def test_balance_between_stocktakings_with_profit_and_loss(self): - """ - This test checks whether the profits and losses and the resulting + """This test checks whether the profits and losses and the resulting balance are calculated correctly. """ # Manipulate the product price timestamps @@ -775,9 +762,7 @@ def test_balance_between_stocktakings_with_profit_and_loss(self): self.assertEqual(result["profit"], 3000) def test_get_latest_non_revoked_stocktakingcollection(self): - """ - This test checks the "get_latest_non_revoked_stocktakingcollection" helper function. - """ + """This test checks the "get_latest_non_revoked_stocktakingcollection" helper function.""" # Insert the default stocktaking collections. self.insert_default_stocktakingcollections() @@ -786,9 +771,7 @@ def test_get_latest_non_revoked_stocktakingcollection(self): self.assertEqual(2, collection.id) # Revoke the latest stocktakingcollection - StocktakingCollection.query.filter( - StocktakingCollection.id == 2 - ).first().revoked = True + StocktakingCollection.query.filter(StocktakingCollection.id == 2).first().revoked = True db.session.commit() # Check the latest collection again @@ -796,35 +779,25 @@ def test_get_latest_non_revoked_stocktakingcollection(self): self.assertEqual(1, collection.id) # Revoke the last remaining non revoked stocktakingcollection - StocktakingCollection.query.filter( - StocktakingCollection.id == 1 - ).first().revoked = True + StocktakingCollection.query.filter(StocktakingCollection.id == 1).first().revoked = True collection = stocktaking_helpers.get_latest_non_revoked_stocktakingcollection() # The result should be None self.assertIsNone(collection) def test_get_latest_stocktaking_of_product(self): - """ - This test checks the "get_latest_stocktaking_of_product" helper function. - """ + """This test checks the "get_latest_stocktaking_of_product" helper function.""" # Insert the default stocktaking collections. self.insert_default_stocktakingcollections() # Check the latest stock - stocktaking = stocktaking_helpers.get_latest_stocktaking_of_product( - product_id=1 - ) + stocktaking = stocktaking_helpers.get_latest_stocktaking_of_product(product_id=1) self.assertEqual(50, stocktaking.count) # Revoke the latest stocktakingcollection - StocktakingCollection.query.filter( - StocktakingCollection.id == 2 - ).first().revoked = True + StocktakingCollection.query.filter(StocktakingCollection.id == 2).first().revoked = True db.session.commit() # Check the latest stock again - stocktaking = stocktaking_helpers.get_latest_stocktaking_of_product( - product_id=1 - ) + stocktaking = stocktaking_helpers.get_latest_stocktaking_of_product(product_id=1) self.assertEqual(100, stocktaking.count) diff --git a/tests/unit/models/__init__.py b/tests/unit/models/__init__.py new file mode 100644 index 0000000..b7a6d38 --- /dev/null +++ b/tests/unit/models/__init__.py @@ -0,0 +1 @@ +"""Contains all unit tests for the helpers.""" diff --git a/tests/test_model_deposit.py b/tests/unit/models/test_model_deposit.py similarity index 75% rename from tests/test_model_deposit.py rename to tests/unit/models/test_model_deposit.py index 9b027ff..d536ac6 100644 --- a/tests/test_model_deposit.py +++ b/tests/unit/models/test_model_deposit.py @@ -2,15 +2,14 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -from shopdb.api import db -from shopdb.models import Deposit, User +from shop_db2.api import db +from shop_db2.models import Deposit, User from tests.base import BaseTestCase class DepositModelTestCase(BaseTestCase): def test_deposit_link_to_its_user(self): - """ - This test checks whether the reference to the user of a deposit is + """This test checks whether the reference to the user of a deposit is working correctly. """ db.session.add(Deposit(user_id=1, amount=1, admin_id=1, comment="Foo")) diff --git a/tests/test_model_purchase.py b/tests/unit/models/test_model_purchase.py similarity index 94% rename from tests/test_model_purchase.py rename to tests/unit/models/test_model_purchase.py index 71a090f..05edc2f 100644 --- a/tests/test_model_purchase.py +++ b/tests/unit/models/test_model_purchase.py @@ -5,8 +5,8 @@ import datetime from copy import copy -from shopdb.api import db -from shopdb.models import Product, ProductPrice, Purchase, User +from shop_db2.api import db +from shop_db2.models import Product, ProductPrice, Purchase, User from tests.base import BaseTestCase @@ -25,8 +25,7 @@ def test_insert_simple_purchase(self): self.assertEqual(user.credit, -product.price) def test_purchase_link_to_its_user(self): - """ - This test checks whether the reference to the user of a purchase is + """This test checks whether the reference to the user of a purchase is working correctly. """ db.session.add(Purchase(user_id=1, product_id=1, amount=1)) @@ -59,10 +58,7 @@ def test_insert_multiple_purchases(self): c = 0 for data in purchase_data: - c -= ( - data["amount"] - * Product.query.filter_by(id=data["product_id"]).first().price - ) + c -= data["amount"] * Product.query.filter_by(id=data["product_id"]).first().price self.assertEqual(user.credit, c) @@ -112,8 +108,8 @@ def test_multi_user_purchases(self): def test_multiple_purchases_update_product_price(self): """This test is designed to ensure that purchases still show - the correct price even after price changes of products.""" - + the correct price even after price changes of products. + """ # Generate timestamps for correct timing of purchases and updates t1 = datetime.datetime.now() - datetime.timedelta(seconds=30) t2 = datetime.datetime.now() - datetime.timedelta(seconds=25) @@ -172,7 +168,8 @@ def test_multiple_purchases_update_product_price(self): def test_purchase_revokes(self): """This unittest is designed to ensure, that purchase revokes will be - applied to the user credit""" + applied to the user credit + """ # Insert some purchases for _ in range(1, 11): purchase = Purchase(user_id=1, product_id=1, amount=1) diff --git a/tests/test_model_tag.py b/tests/unit/models/test_model_tag.py similarity index 96% rename from tests/test_model_tag.py rename to tests/unit/models/test_model_tag.py index 2a2d587..73feb29 100644 --- a/tests/test_model_tag.py +++ b/tests/unit/models/test_model_tag.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -from shopdb.api import db -from shopdb.models import Product, Tag +from shop_db2.api import db +from shop_db2.models import Product, Tag from tests.base import BaseTestCase diff --git a/tests/test_model_user.py b/tests/unit/models/test_model_user.py similarity index 92% rename from tests/test_model_user.py rename to tests/unit/models/test_model_user.py index a4b0ada..1aff1a9 100644 --- a/tests/test_model_user.py +++ b/tests/unit/models/test_model_user.py @@ -2,9 +2,9 @@ # -*- coding: utf-8 -*- __author__ = "g3n35i5" -import shopdb.exceptions as exc -from shopdb.api import db -from shopdb.models import Purchase, User, UserVerification +import shop_db2.exceptions as exc +from shop_db2.api import db +from shop_db2.models import Purchase, User, UserVerification from tests.base import BaseTestCase @@ -53,16 +53,15 @@ def test_verify_user_twice(self): def test_verify_user(self): """Verify a user. We take the last one in the list since all other - users have already been verified.""" + users have already been verified. + """ user = User.query.filter_by(id=4).first() self.assertFalse(user.is_verified) user.verify(admin_id=1, rank_id=1) db.session.commit() user = User.query.filter_by(id=4).first() self.assertTrue(user.is_verified) - verification = UserVerification.query.order_by( - UserVerification.id.desc() - ).first() + verification = UserVerification.query.order_by(UserVerification.id.desc()).first() self.assertEqual(verification.user_id, user.id) self.assertEqual(verification.admin_id, 1) @@ -107,8 +106,7 @@ def test_insert_purchase_as_non_verified_user(self): self.assertEqual(len(purchases), 0) def test_get_favorite_product_ids(self): - """ - This test ensures that the ids of purchased products are returned in + """This test ensures that the ids of purchased products are returned in descending order with respect to the frequency with which they were purchased by the user. """ @@ -133,8 +131,7 @@ def test_get_favorite_product_ids(self): self.assertEqual([3, 2, 1, 4], favorites) def test_get_favorite_product_ids_without_purchases(self): - """ - This test ensures that an empty list for the favorite products is + """This test ensures that an empty list for the favorite products is returned if no purchases have been made by the user yet. """ favorites = User.query.filter_by(id=1).first().favorites diff --git a/tests/unit/test_app_functions.py b/tests/unit/test_app_functions.py new file mode 100644 index 0000000..3397096 --- /dev/null +++ b/tests/unit/test_app_functions.py @@ -0,0 +1,14 @@ +"""Contains all application utility functions.""" + +from pytest import MonkeyPatch + +from shop_db2 import get_app_name, get_app_version + + +def test_get_app_name() -> None: + assert "shop-db2" == get_app_name() + + +def test_get_app_version(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr("shop_db2.__version__", "42.0.0") + assert "42.0.0" == get_app_version() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..96b9304 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1 @@ +"""This module contains utility functions for tests.""" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8a971db --- /dev/null +++ b/tox.ini @@ -0,0 +1,71 @@ +[tox] +envlist = + lint + {py38,py39}-test + combine-test-reports +isolated_build = True + +[gh-actions] +python = + 3.8: py38-test + 3.9: py39-test + +[testenv:lint] +description = Run static checkers. +basepython = py38 +extras = lint +passenv = + RUNNER_OS +commands = + # Check import ordering + isort . --check --diff + # Check formatting + black . --check + # Check type hinting + # mypy . + # Lint source code + # ruff check . + # pylint . {posargs} + # Check that function argument names are consistent between function signatures and docstrings + # pylint --load-plugins pylint.extensions.docparams src {posargs} + + +[testenv:{py38,py39}-test] +description = Run doc tests and unit tests. +package = wheel +extras = test +setenv = + PY_IGNORE_IMPORTMISMATCH=1 + COVERAGE_FILE = reports{/}.coverage.{envname} +passenv = + RUNNER_OS +commands = + # Run tests and doctests from .py files + pytest --junitxml=reports/pytest.xml.{envname} {posargs} src/ tests/ + + +[testenv:combine-test-reports] +description = Combine test and coverage data from multiple test runs. +skip_install = true +setenv = + COVERAGE_FILE = reports/.coverage +depends = {py38,py39}-test +deps = + junitparser + coverage[toml] +commands = + junitparser merge --glob reports/pytest.xml.* reports/pytest.xml + coverage combine --keep + coverage html + coverage xml + +[testenv:build] +description = Build the package. +extras = build +passenv = + RUNNER_OS +commands = + # Clean up build directories + python -c 'from shutil import rmtree; rmtree("build", True); rmtree("dist", True)' + # Build the package + python -m build . diff --git a/wsgi.py b/wsgi.py index 53383e3..318a280 100755 --- a/wsgi.py +++ b/wsgi.py @@ -11,8 +11,9 @@ import gunicorn.app.base from gunicorn.six import iteritems -import configuration as config -from shopdb.api import app, set_app +from shop_db2.api import app, set_app + +import configuration as config # isort: skip def number_of_workers(): @@ -27,11 +28,7 @@ def __init__(self, _app, _options=None): def load_config(self): _config = dict( - [ - (key, value) - for key, value in iteritems(self.options) - if key in self.cfg.settings and value is not None - ] + [(key, value) for key, value in iteritems(self.options) if key in self.cfg.settings and value is not None] ) for key, value in iteritems(_config): self.cfg.set(key.lower(), value) @@ -44,8 +41,7 @@ def load(self): # Check whether the productive database exists. if not os.path.isfile(config.ProductiveConfig.DATABASE_PATH): sys.exit( - "No database found. Please read the documentation and use " - "the setupdb.py script to initialize shop-db." + "No database found. Please read the documentation and use " "the setupdb.py script to initialize shop-db." ) parser = ArgumentParser(description="Starting shop-db2 with a gunicorn server")