diff --git a/README.md b/README.md index 811a9dc4..ed087951 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,14 @@ This repository contains the API endpoints and models for the ART project implem | **RETRY_TIMEOUT** | **Optional** - Number of seconds to wait before retrying an external request (currently to AIS) if an error other than 401 is received. | **10** | | **LOGLEVEL** | **Optional** - Default log level - error, warning, info, debug. | **info** | | **ADMINS** | **Optional** - Email addresses to send error logs to. | **art:art.andela@andela.com,art_group:art@andela.com** | +| **DEFAULT_THRESHOLD** | **REQUIRED** - Minimal asset threshold | **Integer value EG 20**| +| **EMAIL_HOST** | **REQUIRED** - email host. | **smtp.gmail.com** | +| **EMAIL_HOST_USER** | **REQUIRED** - email host user account | | +| **EMAIL_HOST_PASSWORD** | **REQUIRED** - email host user account password | | +| **EMAIL_PORT** | **REQUIRED** - email port. | **587** | +| **EMAIL_USE_TLS** | **REQUIRED** - email TLS. | **True** | +| **EMAIL_SENDER** | **REQUIRED** - email sender's address. | | + ### Project setup #### Installation script @@ -60,6 +68,7 @@ The easiest way to set up is to run `. ./install_art.sh` which does the followin - Install the project dependencies stored in [Pipfile](/Pipfile). Run `pipenv install --dev`. - Run migrations - `python manage.py migrate` - Create cache table `python manage.py createcachetable` +-Open a terminal tab then run the command `python manage.py qcluster` to start django q to enable in sending email notifications #### Development using Docker To use the Docker setup, ensure you have Docker installed then run the following commands: diff --git a/api/send_email.py b/api/send_email.py new file mode 100644 index 00000000..d7befbd8 --- /dev/null +++ b/api/send_email.py @@ -0,0 +1,19 @@ +# Third-Party Imports +from decouple import config +from django_q.tasks import async_task + +# App Imports +from core.constants import MSG, SUBJECT +from core.models.user import User + + +def send_email(data): + """"This method sends emails to users""" + for user in User.objects.filter(is_staff=True): + async_task( + "django.core.mail.send_mail", + SUBJECT, + MSG.format(data.name), + config("EMAIL_SENDER"), + [user.email], + ) diff --git a/api/serializers/assets.py b/api/serializers/assets.py index 443f7ef8..db484751 100644 --- a/api/serializers/assets.py +++ b/api/serializers/assets.py @@ -365,6 +365,7 @@ class Meta: "created_at", "last_modified", "asset_type", + "threshold", ) def to_internal_value(self, data): diff --git a/api/tests/__init__.py b/api/tests/__init__.py index 1b67ad13..6112a383 100644 --- a/api/tests/__init__.py +++ b/api/tests/__init__.py @@ -139,10 +139,12 @@ def setUpClass(cls): name="Sub Category nameseses", asset_category=cls.asset_category ) cls.asset_type = apps.get_model("core", "AssetType").objects.create( - name="Asset Types", asset_sub_category=cls.asset_sub_category + name="Asset Types", asset_sub_category=cls.asset_sub_category, threshold=50 ) cls.test_asset_type = apps.get_model("core", "AssetType").objects.create( - name="Other Asset Types", asset_sub_category=cls.asset_sub_category + name="Other Asset Types", + asset_sub_category=cls.asset_sub_category, + threshold=50, ) cls.asset_make = apps.get_model("core", "AssetMake").objects.create( name="Asset Makes", asset_type=cls.asset_type diff --git a/api/tests/test_asset_type_api.py b/api/tests/test_asset_type_api.py index f4748394..3be3d5fa 100644 --- a/api/tests/test_asset_type_api.py +++ b/api/tests/test_asset_type_api.py @@ -1,12 +1,15 @@ # Standard Library +import os from unittest.mock import patch # Third-Party Imports +from django.test import override_settings from rest_framework.test import APIClient # App Imports from api.tests import APIBaseTestCase -from core.models import AssetType +from core import constants +from core.models import AllocationHistory, AssetType, User client = APIClient() @@ -26,6 +29,7 @@ def test_can_post_asset_type(self, mock_verify_token): data = { "name": "Asset Type Example", "asset_sub_category": self.asset_sub_category.id, + "threshold": 50, } response = client.post( self.asset_type_url, @@ -112,3 +116,29 @@ def test_asset_type_api_orders_asset_types_by_type(self, mock_verify_id_token): # since the asset types are ordered. self.assertEqual(AssetType.objects.count(), len(response.data.get("results"))) self.assertEqual(response.data.get("results")[-1].get("name"), "Samsung") + + @patch("api.send_email.async_task") + @patch("api.authentication.auth.verify_id_token") + @override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend") + def test_email_is_sent_when_asset_type_threshold_critical( + self, mock_verify_token, mail + ): + mock_verify_token.return_value = {"email": self.admin_user.email} + assettype = AssetType( + name="Andelaheadsets", + asset_sub_category=self.asset_sub_category, + threshold=20, + ) + assettype.save() + self.asset_make.asset_type = assettype + self.asset_make.save() + allocate = AllocationHistory( + asset=self.asset_1, current_assignee=self.asset_assignee + ) + allocate.save() + data = User.objects.filter(is_staff=True) + emails = mail._mock_call_args_list[0] + self.assertEqual(emails[0][1], constants.SUBJECT) + self.assertEqual(emails[0][3], os.getenv("EMAIL_SENDER")) + self.assertEqual(emails[0][2], constants.MSG.format(assettype.name)) + self.assertEqual(emails[0][4][0], data[0].email) diff --git a/core/constants.py b/core/constants.py index bc2c3059..86da9314 100644 --- a/core/constants.py +++ b/core/constants.py @@ -141,3 +141,6 @@ (WARNING_NOTIFICATION, WARNING_NOTIFICATION), (INFO_NOTIFICATION, INFO_NOTIFICATION), ) +SUBJECT = "STOCK LEVEL CRITICAL" + +MSG = "Dear Admin, The stock level for the Asset type {} have gone below the minimum threshold." diff --git a/core/migrations/0053_assettype_threshold.py b/core/migrations/0053_assettype_threshold.py new file mode 100644 index 00000000..ac3b142f --- /dev/null +++ b/core/migrations/0053_assettype_threshold.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.10 on 2019-08-26 12:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0052_notifications'), + ] + + operations = [ + migrations.AddField( + model_name='assettype', + name='threshold', + field=models.IntegerField(default=0), + ), + ] diff --git a/core/models/asset.py b/core/models/asset.py index 59c1b8ec..c8b2c458 100644 --- a/core/models/asset.py +++ b/core/models/asset.py @@ -5,11 +5,13 @@ from datetime import datetime # Third-Party Imports +from decouple import config from django.conf import settings from django.core.exceptions import ValidationError from django.db import models # App Imports +from api.send_email import send_email from core import constants from core.managers import CaseInsensitiveManager from core.slack_bot import SlackIntegration @@ -26,7 +28,7 @@ def user_abstract(user, filename): :params: user -> user object :params: filename -> string """ - return f'user_{user}_{filename}' + return f"user_{user}_{filename}" class AssetCategory(models.Model): @@ -92,6 +94,7 @@ class AssetType(models.Model): last_modified = models.DateTimeField(auto_now=True, editable=False) asset_sub_category = models.ForeignKey("AssetSubCategory", on_delete=models.PROTECT) has_specs = models.BooleanField(default=False) + threshold = models.IntegerField(default=0) objects = CaseInsensitiveManager() @@ -542,6 +545,13 @@ def save(self, *args, **kwargs): self.previous_assignee = None try: super().save(*args, **kwargs) + if self.previous_assignee is None: + threshold_data = self.asset.model_number.asset_make.asset_type + threshold_data.threshold = threshold_data.threshold - 1 + if threshold_data.threshold <= int(config("DEFAULT_THRESHOLD")): + send_email(threshold_data) + threshold_data.save() + except Exception: raise else: @@ -619,10 +629,10 @@ class AssetIncidentReport(models.Model): loss_of_property = models.TextField(null=True, blank=True) witnesses = models.TextField(null=True, blank=True) police_abstract_obtained = models.CharField(max_length=255) - submitted_by = models.ForeignKey('User', null=True, on_delete=models.PROTECT) + submitted_by = models.ForeignKey("User", null=True, on_delete=models.PROTECT) created_at = models.DateTimeField(default=datetime.now, editable=False) police_abstract = models.FileField( - 'Police Abstract', upload_to=user_abstract, blank=True + "Police Abstract", upload_to=user_abstract, blank=True ) def __str__(self): diff --git a/settings/base.py b/settings/base.py index 81cd410e..3fcc71cb 100644 --- a/settings/base.py +++ b/settings/base.py @@ -181,7 +181,7 @@ REDOC_SETTINGS = {"LAZY_RENDERING": True} -# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_HOST = config("EMAIL_HOST", None) EMAIL_PORT = config("EMAIL_PORT", None) @@ -205,5 +205,3 @@ 'LOCATION': 'art_cache', } } -# log emails in console rather than sending them -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'