diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 0000000..345c303
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,6 @@
+name-template: v$NEXT_PATCH_VERSION
+tag-template: v$NEXT_PATCH_VERSION
+template: |
+ ## What’s Changed
+
+ $CHANGES
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 98c503c..a491999 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,3 +1,4 @@
+exclude: '/snapshots/'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
diff --git a/.pylintrc b/.pylintrc
index 43d2e79..51afeeb 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -7,7 +7,7 @@ extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
-ignore=CVS,tests,migrations,local_settings.py
+ignore=CVS,tests,migrations,local_settings.py,snapshots
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
@@ -126,7 +126,8 @@ disable=print-statement,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
- dict-values-not-iterating
+ dict-values-not-iterating,
+ bad-continuation
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
diff --git a/MANIFEST.in b/MANIFEST.in
index 63636f2..f72dde5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,3 +2,5 @@ include LICENSE
include README.md
recursive-include small_small_hr/static *
recursive-include small_small_hr/templates *
+
+recursive-exclude tests *
diff --git a/requirements/dev.in b/requirements/dev.in
index 4107e7c..9b989f4 100644
--- a/requirements/dev.in
+++ b/requirements/dev.in
@@ -20,3 +20,5 @@ black
pre-commit
mypy
tblib
+snapshottest
+freezegun
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 3e1d5ce..1e597e5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -6,77 +6,88 @@
#
appdirs==1.4.4 # via black, virtualenv
-asgiref==3.2.7 # via django
-astroid==2.4.1 # via pylint
+asgiref==3.2.10 # via django
+astroid==2.4.2 # via pylint
attrs==19.3.0 # via black
autopep8==1.5.3
babel==2.8.0 # via django-phonenumber-field
-backcall==0.1.0 # via ipython
+backcall==0.2.0 # via ipython
black==19.10b0
cfgv==3.1.0 # via pre-commit
click==7.1.2 # via black
coverage==5.1
decorator==4.4.2 # via ipython, traitlets
distlib==0.3.0 # via virtualenv
+django-braces==1.14.0 # via django-model-reviews
+django-contrib-comments==1.9.2 # via django-model-reviews
django-crispy-forms==1.9.1
+django-js-asset==1.2.2 # via django-mptt
+django-model-reviews==1.0.2
+django-mptt==0.11.0
django-phonenumber-field==4.0.0
django-private-storage==2.2.2
-django==3.0.7 # via django-phonenumber-field, djangorestframework, model-mommy
+django==3.0.7 # via django-braces, django-contrib-comments, django-model-reviews, django-mptt, django-phonenumber-field, djangorestframework, model-mommy
djangorestframework==3.11.0
+fastdiff==0.2.0 # via snapshottest
filelock==3.0.12 # via tox, virtualenv
-flake8==3.8.2
-identify==1.4.19 # via pre-commit
+flake8==3.8.3
+freezegun==0.3.15
+identify==1.4.20 # via pre-commit
importlib-metadata==1.6.1 # via flake8, importlib-resources, pluggy, pre-commit, tox, virtualenv
-importlib-resources==1.5.0 # via pre-commit, virtualenv
-ipdb==0.13.2
+importlib-resources==2.0.1 # via pre-commit, virtualenv
+ipdb==0.13.3
ipython-genutils==0.2.0 # via traitlets
ipython==7.15.0 # via ipdb
isort==4.3.21
-jedi==0.17.0 # via ipython
+jedi==0.17.1 # via ipython
lazy-object-proxy==1.4.3 # via astroid
mccabe==0.6.1 # via flake8, pylint
model-mommy==2.0.0
mypy-extensions==0.4.3 # via mypy
-mypy==0.780
+mypy==0.782
nodeenv==1.4.0 # via pre-commit
packaging==20.4 # via tox
parso==0.7.0 # via jedi
pathspec==0.8.0 # via black
pep8==1.7.1
pexpect==4.8.0 # via ipython
-phonenumberslite==8.12.5
+phonenumberslite==8.12.6
pickleshare==0.7.5 # via ipython
pillow==7.1.2
pluggy==0.13.1 # via tox
-pre-commit==2.4.0
+pre-commit==2.5.1
prompt-toolkit==3.0.5 # via ipython
psycopg2-binary==2.8.5
ptyprocess==0.6.0 # via pexpect
-py==1.8.1 # via tox
+py==1.8.2 # via tox
pycodestyle==2.6.0
pydocstyle==5.0.2
pyflakes==2.2.0 # via flake8
pygments==2.6.1 # via ipython
pylint-django==2.0.15
pylint-plugin-utils==0.6 # via pylint-django
-pylint==2.5.2
+pylint==2.5.3
pyparsing==2.4.7 # via packaging
+python-dateutil==2.8.1 # via freezegun
pytz==2020.1 # via babel, django
pyyaml==5.3.1 # via pre-commit
-regex==2020.5.14 # via black
-six==1.15.0 # via astroid, packaging, tox, traitlets, virtualenv
+regex==2020.6.8 # via black
+six==1.15.0 # via astroid, django-braces, django-contrib-comments, freezegun, packaging, python-dateutil, snapshottest, tox, traitlets, virtualenv
+snapshottest==0.5.1
snowballstemmer==2.0.0 # via pydocstyle
sorl-thumbnail==12.6.3
sqlparse==0.3.1 # via django
tblib==1.6.0
+termcolor==1.1.0 # via snapshottest
toml==0.10.1 # via autopep8, black, pre-commit, pylint, tox
tox==3.15.2
traitlets==4.3.3 # via ipython
typed-ast==1.4.1 # via astroid, black, mypy
typing-extensions==3.7.4.2 # via mypy
-virtualenv==20.0.21 # via pre-commit, tox
+virtualenv==20.0.25 # via pre-commit, tox
voluptuous==0.11.7
-wcwidth==0.2.3 # via prompt-toolkit
+wasmer==0.4.1 # via fastdiff
+wcwidth==0.2.5 # via prompt-toolkit
wrapt==1.12.1 # via astroid
yapf==0.30.0
zipp==3.1.0 # via importlib-metadata, importlib-resources
diff --git a/setup.cfg b/setup.cfg
index c445da7..3475aba 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,7 +12,7 @@ line_length = 88
[flake8]
max-line-length=90
-exclude = migrations
+exclude = migrations, snapshots
[pycodestyle]
max-line-length=90
diff --git a/setup.py b/setup.py
index 9e7b673..0300348 100644
--- a/setup.py
+++ b/setup.py
@@ -1,19 +1,34 @@
-"""
-Setup.py for small_small_hr
-"""
-from os import path
+"""Setup.py for small_small_hr."""
+import os
+import sys
from setuptools import find_packages, setup
+import small_small_hr as sshr
+
# read the contents of your README file
with open(
- path.join(path.abspath(path.dirname(__file__)), "README.md"),
- encoding="utf-8") as f:
+ os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md"),
+ encoding="utf-8",
+) as f:
LONG_DESCRIPTION = f.read()
+# convenience command to publish
+if sys.argv[-1] == "publish":
+ if os.system("pip freeze | grep twine"):
+ print("twine not installed.\nUse `pip install twine`.\nExiting.")
+ sys.exit()
+ os.system("rm -rf build/ *.egg-info/")
+ os.system("python setup.py sdist bdist_wheel")
+ os.system("twine upload dist/* --skip-existing")
+ print("You probably want to also tag the version now:")
+ print(f" git tag -a v{sshr.__version__} -m 'version {sshr.__version__}'")
+ print(" git push --tags")
+ sys.exit()
+
setup(
name="small-small-hr",
- version=__import__("small_small_hr").__version__,
+ version=sshr.__version__,
description="Minimal human resource management app for Django",
long_description=LONG_DESCRIPTION,
long_description_content_type="text/markdown",
@@ -21,19 +36,21 @@
author="Kelvin Jayanoris",
author_email="kelvin@jayanoris.com",
url="https://github.com/moshthepitt/small-small-hr",
- packages=find_packages(exclude=["docs", "tests"]),
+ packages=find_packages(exclude=["docs", "*.egg-info", "build", "tests.*", "tests"]),
install_requires=[
- "Django >= 2.0.11",
+ "Django >= 2.2",
"voluptuous",
"psycopg2-binary",
"sorl-thumbnail",
"django-private-storage",
+ "django-model-reviews",
"phonenumberslite",
"django-phonenumber-field",
"django-crispy-forms",
"djangorestframework",
"sorl-thumbnail",
"Pillow",
+ "django-mptt",
],
classifiers=[
"Programming Language :: Python",
@@ -44,4 +61,5 @@
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
],
+ include_package_data=True,
)
diff --git a/small_small_hr/__init__.py b/small_small_hr/__init__.py
index 0ab9e5e..1d0da64 100644
--- a/small_small_hr/__init__.py
+++ b/small_small_hr/__init__.py
@@ -1,7 +1,5 @@
-"""
-Main init file for small_small_hr
-"""
-VERSION = (0, 1, 9)
+"""Main init file for small_small_hr."""
+VERSION = (0, 2, 0)
__version__ = ".".join(str(v) for v in VERSION)
# pylint: disable=invalid-name
default_app_config = "small_small_hr.apps.SmallSmallHrConfig" # noqa
diff --git a/small_small_hr/apps.py b/small_small_hr/apps.py
index d96c3d1..0dda60a 100644
--- a/small_small_hr/apps.py
+++ b/small_small_hr/apps.py
@@ -2,23 +2,45 @@
Apps module for small-small-hr
"""
from django.apps import AppConfig
+from django.db.models.signals import post_migrate
from django.utils.translation import gettext_lazy as _
+def mptt_callback(sender, **kwargs): # pylint: disable=unused-argument
+ """
+ Rebuild mptt tree.
+
+ We need to do this so that for existing objects we can rebuild the mptt tree
+ and set correct values for all the mptt fields. This is necessary because we
+ need to remove the defaults placed in the migrations file.
+ """
+ # pylint: disable=import-outside-toplevel
+ from small_small_hr.models import StaffProfile
+
+ StaffProfile.objects.rebuild()
+
+
class SmallSmallHrConfig(AppConfig):
"""Apps config class."""
- name = 'small_small_hr'
- app_label = 'small_small_hr'
+
+ name = "small_small_hr"
+ app_label = "small_small_hr"
verbose_name = _("Small Small HR")
def ready(self):
"""Do stuff when the app is ready."""
# pylint: disable=import-outside-toplevel,unused-import
+
+ # post migrate function
+ post_migrate.connect(mptt_callback, sender=self)
+
+ # signals
import small_small_hr.signals # noqa
# set up app settings
from django.conf import settings
import small_small_hr.settings as defaults
+
for name in dir(defaults):
if name.isupper() and not hasattr(settings, name):
setattr(settings, name, getattr(defaults, name))
diff --git a/small_small_hr/constants.py b/small_small_hr/constants.py
new file mode 100644
index 0000000..f229471
--- /dev/null
+++ b/small_small_hr/constants.py
@@ -0,0 +1,9 @@
+"""Constants."""
+HOURS = "hours"
+MINUTES = "minutes"
+STAFF = "staff"
+OVERTIME_APPLICATION_EMAIL_TEMPLATE = "overtime_application"
+OVERTIME_COMPLETED_EMAIL_TEMPLATE = "overtime_completed"
+LEAVE_APPLICATION_EMAIL_TEMPLATE = "leave_application"
+LEAVE_COMPLETED_EMAIL_TEMPLATE = "leave_completed"
+EMAIL_TEMPLATE_PATH = "small_small_hr/email"
diff --git a/small_small_hr/emails.py b/small_small_hr/emails.py
index b53ec32..d2b991f 100644
--- a/small_small_hr/emails.py
+++ b/small_small_hr/emails.py
@@ -1,173 +1,76 @@
-"""
-Emails module for scam app
-"""
-from django.conf import settings
-from django.contrib.sites.models import Site
-from django.core.mail import EmailMultiAlternatives
-from django.template.loader import render_to_string
-from django.utils.translation import ugettext as _
-
-from small_small_hr.models import Leave, OverTime
-
-
-def send_email( # pylint: disable=too-many-arguments,too-many-locals,bad-continuation
- name: str,
- email: str,
- subject: str,
- message: str,
- obj: object = None,
- cc_list: list = None,
- template: str = "generic",
- template_path: str = "small_small_hr/email",
-):
- """
- Sends a generic email
-
- :param name: name of person
- :param email: email address to send to
- :param subject: the email's subject
- :param message: the email's body text
- :param obj: the object in question
- :param cc_list: the list of email address to "CC"
- :param template: the template to use
- """
- context = {
- "name": name,
- "subject": subject,
- "message": message,
- "object": obj,
- "SITE": Site.objects.get_current(),
- }
- email_subject = render_to_string(
- f"{template_path}/{template}_email_subject.txt", context
- ).replace("\n", "")
- email_txt_body = render_to_string(
- f"{template_path}/{template}_email_body.txt", context
- )
- email_html_body = render_to_string(
- f"{template_path}/{template}_email_body.html", context
- ).replace("\n", "")
-
- subject = email_subject
- from_email = settings.DEFAULT_FROM_EMAIL
- to_email = f"{name} <{email}>"
- text_content = email_txt_body
- html_content = email_html_body
- msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
- if cc_list:
- msg.cc = cc_list
- msg.attach_alternative(html_content, "text/html")
-
- return msg.send(fail_silently=True)
-
-
-def leave_application_email(leave_obj: Leave):
- """
- Sends an email to admins when a leave application is made
- """
- msg = getattr(
- settings,
- "SSHR_LEAVE_APPLICATION_EMAIL_TXT",
- _("There has been a new leave application. Please log in to process " "it."),
- )
- subj = getattr(
- settings, "SSHR_LEAVE_APPLICATION_EMAIL_SUBJ", _("New Leave Application")
- )
- admin_emails = settings.SSHR_ADMIN_LEAVE_EMAILS
-
- for admin_email in admin_emails:
+"""Emails module for scam app."""
+from model_reviews.emails import get_display_name, send_email
+from model_reviews.models import ModelReview, Reviewer
+
+from small_small_hr.constants import (
+ LEAVE_APPLICATION_EMAIL_TEMPLATE,
+ LEAVE_COMPLETED_EMAIL_TEMPLATE,
+ OVERTIME_APPLICATION_EMAIL_TEMPLATE,
+ OVERTIME_COMPLETED_EMAIL_TEMPLATE,
+)
+
+
+def send_request_for_leave_review(reviewer: Reviewer):
+ """Send email requesting a Leave review to one reviewer."""
+ if reviewer.user.email:
+ source = reviewer.review.content_object
send_email(
- name=settings.SSHR_ADMIN_NAME,
- email=admin_email,
- subject=subj,
- message=msg,
- obj=leave_obj,
- template="leave_application",
+ name=get_display_name(reviewer.user),
+ email=reviewer.user.email,
+ subject=source.review_request_email_subject,
+ message=source.review_request_email_body,
+ obj=reviewer.review,
+ cc_list=None,
+ template=LEAVE_APPLICATION_EMAIL_TEMPLATE,
+ template_path=source.email_template_path,
)
-def leave_processed_email(leave_obj: Leave):
- """
- Sends an email to admins when a leave application is processed
- """
- if leave_obj.staff.user.email:
- msg = getattr(
- settings,
- "SSHR_LEAVE_PROCESSED_EMAIL_TXT",
- _(
- f"You leave application status is "
- f"{leave_obj.get_status_display()}. Log in for more info."
- ),
- )
- subj = getattr(
- settings,
- "SSHR_LEAVE_PROCESSED_EMAIL_SUBJ",
- _("Your leave application has been processed"),
- )
-
+def send_request_for_overtime_review(reviewer: Reviewer):
+ """Send email requesting a OverTime review to one reviewer."""
+ if reviewer.user.email:
+ source = reviewer.review.content_object
send_email(
- name=leave_obj.staff.get_name(),
- email=leave_obj.staff.user.email,
- subject=subj,
- message=msg,
- obj=leave_obj,
- cc_list=settings.SSHR_ADMIN_LEAVE_EMAILS,
+ name=get_display_name(reviewer.user),
+ email=reviewer.user.email,
+ subject=source.review_request_email_subject,
+ message=source.review_request_email_body,
+ obj=reviewer.review,
+ cc_list=None,
+ template=OVERTIME_APPLICATION_EMAIL_TEMPLATE,
+ template_path=source.email_template_path,
)
-def overtime_application_email(overtime_obj: OverTime):
- """
- Sends an email to admins when an overtime application is made
- """
- msg = getattr(
- settings,
- "SSHR_OVERTIME_APPLICATION_EMAIL_TXT",
- _(
- "There has been a new overtime application. Please log in to "
- "process it."
- ),
- )
- subj = getattr(
- settings, "SSHR_OVERTIME_APPLICATION_EMAIL_SUBJ", _("New Overtime Application")
- )
- admin_emails = settings.SSHR_ADMIN_OVERTIME_EMAILS
-
- for admin_email in admin_emails:
- send_email(
- name=settings.SSHR_ADMIN_NAME,
- email=admin_email,
- subject=subj,
- message=msg,
- obj=overtime_obj,
- template="overtime_application",
- )
-
-
-def overtime_processed_email(overtime_obj: OverTime):
- """
- Sends an email to admins when an overtime application is processed
- """
- if overtime_obj.staff.user.email:
-
- msg = getattr(
- settings,
- "SSHR_OVERTIME_PROCESSED_EMAIL_TXT",
- _(
- f"You overtime application status is "
- f"{overtime_obj.get_status_display()}. Log in for more info."
- ),
- )
- subj = getattr(
- settings,
- "SSHR_OVERTIME_PROCESSED_EMAIL_SUBJ",
- _("Your overtime application has been processed"),
- )
-
- send_email(
- name=overtime_obj.staff.get_name(),
- email=overtime_obj.staff.user.email,
- subject=subj,
- message=msg,
- obj=overtime_obj,
- cc_list=settings.SSHR_ADMIN_OVERTIME_EMAILS,
- )
+def send_leave_review_complete_notice(review_obj: ModelReview):
+ """Send notice that Leave review is complete."""
+ if not review_obj.needs_review() and review_obj.user:
+ if review_obj.user.email:
+ source = review_obj.content_object
+ send_email(
+ name=get_display_name(review_obj.user),
+ email=review_obj.user.email,
+ subject=source.review_complete_email_subject,
+ message=source.review_complete_email_body,
+ obj=review_obj,
+ cc_list=None,
+ template=LEAVE_COMPLETED_EMAIL_TEMPLATE,
+ template_path=source.email_template_path,
+ )
+
+
+def send_overtime_review_complete_notice(review_obj: ModelReview):
+ """Send notice that OverTime review is complete."""
+ if not review_obj.needs_review() and review_obj.user:
+ if review_obj.user.email:
+ source = review_obj.content_object
+ send_email(
+ name=get_display_name(review_obj.user),
+ email=review_obj.user.email,
+ subject=source.review_complete_email_subject,
+ message=source.review_complete_email_body,
+ obj=review_obj,
+ cc_list=None,
+ template=OVERTIME_COMPLETED_EMAIL_TEMPLATE,
+ template_path=source.email_template_path,
+ )
diff --git a/small_small_hr/forms.py b/small_small_hr/forms.py
index 81cffda..2ba0dfe 100644
--- a/small_small_hr/forms.py
+++ b/small_small_hr/forms.py
@@ -1,6 +1,4 @@
-"""
-Forms module for small small hr
-"""
+"""Forms module for small small hr."""
from datetime import datetime, time
from django import forms
@@ -16,218 +14,189 @@
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
-from small_small_hr.emails import (leave_application_email,
- overtime_application_email)
-from small_small_hr.models import (TWOPLACES, AnnualLeave, FreeDay, Leave,
- OverTime, Role, StaffDocument, StaffProfile)
+from small_small_hr.models import (
+ TWOPLACES,
+ AnnualLeave,
+ FreeDay,
+ Leave,
+ OverTime,
+ Role,
+ StaffDocument,
+ StaffProfile,
+)
class AnnualLeaveForm(forms.ModelForm):
- """
- Form used when managing AnnualLeave
- """
+ """Form used when managing AnnualLeave."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = AnnualLeave
- fields = [
- 'staff',
- 'year',
- 'leave_type',
- 'allowed_days',
- 'carried_over_days'
- ]
+ fields = ["staff", "year", "leave_type", "allowed_days", "carried_over_days"]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
if not self.instance:
- self.fields['year'].initial = datetime.today().year
+ self.fields["year"].initial = datetime.today().year
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'annual-leave-form'
+ self.helper.form_id = "annual-leave-form"
self.helper.layout = Layout(
- Field('staff',),
- Field('year',),
- Field('leave_type',),
- Field('allowed_days',),
- Field('carried_over_days'),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff",),
+ Field("year",),
+ Field("leave_type",),
+ Field("allowed_days",),
+ Field("carried_over_days"),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
class RoleForm(forms.ModelForm):
- """
- Form used when managing Role objects
- """
+ """Form used when managing Role objects."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = Role
- fields = [
- 'name',
- 'description'
- ]
+ fields = ["name", "description"]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'role-form'
+ self.helper.form_id = "role-form"
self.helper.layout = Layout(
- Field('name',),
- Field('description',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("name",),
+ Field("description",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
class FreeDayForm(forms.ModelForm):
- """
- Form used when managing FreeDay objects
- """
+ """Form used when managing FreeDay objects."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = FreeDay
- fields = [
- 'name',
- 'date'
- ]
+ fields = ["name", "date"]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'freeday-form'
+ self.helper.form_id = "freeday-form"
self.helper.layout = Layout(
- Field('name',),
- Field('date',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("name",),
+ Field("date",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
class OverTimeForm(forms.ModelForm):
- """
- Form used when managing OverTime objects
- """
+ """Form used when managing OverTime objects."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = OverTime
fields = [
- 'staff',
- 'date',
- 'start',
- 'end',
- 'reason',
- 'status',
- 'comments'
+ "staff",
+ "date",
+ "start",
+ "end",
+ "review_reason",
+ "review_status",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'overtime-form'
+ self.helper.form_id = "overtime-form"
self.helper.layout = Layout(
- Field('staff',),
- Field('date',),
- Field('start',),
- Field('end',),
- Field('reason',),
- Field('status',),
- Field('comments'),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff",),
+ Field("date",),
+ Field("start",),
+ Field("end",),
+ Field("review_reason",),
+ Field("review_status",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
def clean(self):
- """
- Custom clean method
- """
+ """Clean all the form fields."""
cleaned_data = super().clean()
- end = cleaned_data.get('end')
- start = cleaned_data.get('start')
- date = cleaned_data.get('date')
- staff = cleaned_data.get('staff')
- status = cleaned_data.get('status')
+ end = cleaned_data.get("end")
+ start = cleaned_data.get("start")
+ date = cleaned_data.get("date")
+ staff = cleaned_data.get("staff")
+ review_status = cleaned_data.get("review_status")
# end must be later than start
if end <= start:
- self.add_error('end', _("end must be greater than start"))
+ self.add_error("end", _("end must be greater than start"))
# must not overlap within the same date unless being rejected
# pylint: disable=no-member
overlap_qs = OverTime.objects.filter(
- date=date, staff=staff, status=OverTime.APPROVED).filter(
- Q(start__gte=start) & Q(end__lte=end))
+ date=date, staff=staff, review_status=OverTime.APPROVED
+ ).filter(Q(start__gte=start) & Q(end__lte=end))
if self.instance is not None:
overlap_qs = overlap_qs.exclude(id=self.instance.id)
- if overlap_qs.exists() and status != OverTime.REJECTED:
- msg = _('you cannot have overlapping overtime hours on the '
- 'same day')
- self.add_error('start', msg)
- self.add_error('end', msg)
- self.add_error('date', msg)
+ if overlap_qs.exists() and review_status != OverTime.REJECTED:
+ msg = _("you cannot have overlapping overtime hours on the " "same day")
+ self.add_error("start", msg)
+ self.add_error("end", msg)
+ self.add_error("date", msg)
class ApplyOverTimeForm(OverTimeForm):
- """
- Form used when applying for overtime
- """
+ """Form used when applying for overtime."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = OverTime
fields = [
- 'staff',
- 'date',
- 'start',
- 'end',
- 'reason',
+ "staff",
+ "date",
+ "start",
+ "end",
+ "review_reason",
]
def __init__(self, *args, **kwargs):
+ """Initialize the form."""
super().__init__(*args, **kwargs)
- self.request = kwargs.pop('request', None)
+ self.request = kwargs.pop("request", None)
if self.request:
# pylint: disable=no-member
try:
@@ -235,182 +204,168 @@ def __init__(self, *args, **kwargs):
except StaffProfile.DoesNotExist:
pass
else:
- self.fields['staff'].queryset = StaffProfile.objects.filter(
- id=self.request.user.staffprofile.id)
+ self.fields["staff"].queryset = StaffProfile.objects.filter(
+ id=self.request.user.staffprofile.id
+ )
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'overtime-application-form'
+ self.helper.form_id = "overtime-application-form"
self.helper.layout = Layout(
- Field('staff', type="hidden"),
- Field('date',),
- Field('start',),
- Field('end',),
- Field('reason',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff", type="hidden"),
+ Field("date",),
+ Field("start",),
+ Field("end",),
+ Field("review_reason",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
def save(self, commit=True):
- """
- Custom save method
- """
+ """Save the form."""
overtime = super().save()
- overtime_application_email(overtime_obj=overtime)
return overtime
class LeaveForm(forms.ModelForm):
- """
- Form used when managing Leave objects
- """
- start = forms.DateField(label=_('Start Date'), required=True)
- end = forms.DateField(label=_('End Date'), required=True)
+ """Form used when managing Leave objects."""
+
+ start = forms.DateField(label=_("Start Date"), required=True)
+ end = forms.DateField(label=_("End Date"), required=True)
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = Leave
fields = [
- 'staff',
- 'leave_type',
- 'start',
- 'end',
- 'reason',
- 'status',
- 'comments'
+ "staff",
+ "leave_type",
+ "start",
+ "end",
+ "review_reason",
+ "review_status",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'leave-form'
+ self.helper.form_id = "leave-form"
self.helper.layout = Layout(
- Field('staff',),
- Field('leave_type',),
- Field('start',),
- Field('end',),
- Field('reason',),
- Field('status',),
- Field('comments'),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff",),
+ Field("leave_type",),
+ Field("start",),
+ Field("end",),
+ Field("review_reason",),
+ Field("review_status",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
def clean_start(self):
- """
- clean start field
- """
- data = self.cleaned_data['start']
+ """Clean start field."""
+ data = self.cleaned_data["start"]
data = datetime.combine(
date=data,
time=time(settings.SSHR_DEFAULT_TIME, 0, 0, 0),
- tzinfo=pytz.timezone(settings.TIME_ZONE))
+ tzinfo=pytz.timezone(settings.TIME_ZONE),
+ )
return data
def clean_end(self):
- """
- clean end field
- """
- data = self.cleaned_data['end']
+ """Clean end field."""
+ data = self.cleaned_data["end"]
data = datetime.combine(
date=data,
time=time(settings.SSHR_DEFAULT_TIME, 0, 0, 0),
- tzinfo=pytz.timezone(settings.TIME_ZONE))
+ tzinfo=pytz.timezone(settings.TIME_ZONE),
+ )
return data
def clean(self):
- """
- Custom clean method
- """
+ """Clean all the form fields."""
cleaned_data = super().clean()
- leave_type = cleaned_data.get('leave_type')
- staff = cleaned_data.get('staff')
- end = cleaned_data.get('end')
- start = cleaned_data.get('start')
- status = cleaned_data.get('status')
+ leave_type = cleaned_data.get("leave_type")
+ staff = cleaned_data.get("staff")
+ end = cleaned_data.get("end")
+ start = cleaned_data.get("start")
+ review_status = cleaned_data.get("review_status")
if all([staff, leave_type, start, end]):
# end year and start year must be the same
if end.year != start.year:
- msg = _('start and end must be from the same year')
- self.add_error('start', msg)
- self.add_error('end', msg)
+ msg = _("start and end must be from the same year")
+ self.add_error("start", msg)
+ self.add_error("end", msg)
# end must be later than start
if end < start:
- self.add_error('end', _("end must be greater than start"))
+ self.add_error("end", _("end must be greater than start"))
if not settings.SSHR_ALLOW_OVERSUBSCRIBE:
# staff profile must have sufficient sick days
if leave_type == Leave.SICK:
sick_days = staff.get_available_sick_days(year=start.year)
if (end - start).days > sick_days:
- msg = _('Not enough sick days. Available sick days '
- f'are {sick_days.quantize(TWOPLACES)}')
- self.add_error('start', msg)
- self.add_error('end', msg)
+ msg = _(
+ "Not enough sick days. Available sick days "
+ f"are {sick_days.quantize(TWOPLACES)}"
+ )
+ self.add_error("start", msg)
+ self.add_error("end", msg)
# staff profile must have sufficient leave days
if leave_type == Leave.REGULAR:
- leave_days = staff.get_available_leave_days(
- year=start.year)
+ leave_days = staff.get_available_leave_days(year=start.year)
if (end - start).days > leave_days:
- msg = _('Not enough leave days. Available leave days '
- f'are {leave_days.quantize(TWOPLACES)}')
- self.add_error('start', msg)
- self.add_error('end', msg)
+ msg = _(
+ "Not enough leave days. Available leave days "
+ f"are {leave_days.quantize(TWOPLACES)}"
+ )
+ self.add_error("start", msg)
+ self.add_error("end", msg)
# must not overlap unless it is being rejected
# pylint: disable=no-member
overlap_qs = Leave.objects.filter(
- staff=staff,
- status=Leave.APPROVED,
- leave_type=leave_type).filter(
- Q(start__gte=start) & Q(end__lte=end))
+ staff=staff, review_status=Leave.APPROVED, leave_type=leave_type
+ ).filter(Q(start__gte=start) & Q(end__lte=end))
if self.instance is not None:
overlap_qs = overlap_qs.exclude(id=self.instance.id)
- if overlap_qs.exists() and status != Leave.REJECTED:
- msg = _('you cannot have overlapping leave days')
- self.add_error('start', msg)
- self.add_error('end', msg)
+ if overlap_qs.exists() and review_status != Leave.REJECTED:
+ msg = _("you cannot have overlapping leave days")
+ self.add_error("start", msg)
+ self.add_error("end", msg)
class ApplyLeaveForm(LeaveForm):
- """
- Form used when applying for Leave
- """
+ """Form used when applying for Leave."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = Leave
fields = [
- 'staff',
- 'leave_type',
- 'start',
- 'end',
- 'reason',
+ "staff",
+ "leave_type",
+ "start",
+ "end",
+ "review_reason",
]
def __init__(self, *args, **kwargs):
+ """Initialize the form."""
super().__init__(*args, **kwargs)
- self.request = kwargs.pop('request', None)
+ self.request = kwargs.pop("request", None)
if self.request:
# pylint: disable=no-member
try:
@@ -418,96 +373,86 @@ def __init__(self, *args, **kwargs):
except StaffProfile.DoesNotExist:
pass
else:
- self.fields['staff'].queryset = StaffProfile.objects.filter(
- id=self.request.user.staffprofile.id)
+ self.fields["staff"].queryset = StaffProfile.objects.filter(
+ id=self.request.user.staffprofile.id
+ )
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'leave-application-form'
+ self.helper.form_id = "leave-application-form"
self.helper.layout = Layout(
- Field('staff', type="hidden"),
- Field('leave_type',),
- Field('start',),
- Field('end',),
- Field('reason',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff", type="hidden"),
+ Field("leave_type",),
+ Field("start",),
+ Field("end",),
+ Field("review_reason",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
def save(self, commit=True):
- """
- Custom save method
- """
+ """Save the form."""
leave = super().save()
- leave_application_email(leave_obj=leave)
return leave
class StaffDocumentForm(forms.ModelForm):
- """
- Form used when managing StaffDocument objects
- """
+ """Form used when managing StaffDocument objects."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = StaffDocument
fields = [
- 'staff',
- 'name',
- 'description',
- 'public',
- 'file',
+ "staff",
+ "name",
+ "description",
+ "public",
+ "file",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
if self.instance and self.instance.file:
- self.fields['file'].required = False
+ self.fields["file"].required = False
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'staffdocument-form'
+ self.helper.form_id = "staffdocument-form"
self.helper.layout = Layout(
- Field('staff',),
- Field('name',),
- Field('description',),
- Field('file',),
- Field('public',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff",),
+ Field("name",),
+ Field("description",),
+ Field("file",),
+ Field("public",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
class UserStaffDocumentForm(forms.ModelForm):
- """
- Form used when managing one's own StaffDocument objects
- """
+ """Form used when managing one's own StaffDocument objects."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = StaffDocument
fields = [
- 'staff',
- 'name',
- 'description',
- 'file',
+ "staff",
+ "name",
+ "description",
+ "file",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
if self.request:
# pylint: disable=no-member
@@ -516,319 +461,313 @@ def __init__(self, *args, **kwargs):
except StaffProfile.DoesNotExist:
pass
else:
- self.fields['staff'].queryset = StaffProfile.objects.filter(
- id=self.request.user.staffprofile.id)
+ self.fields["staff"].queryset = StaffProfile.objects.filter(
+ id=self.request.user.staffprofile.id
+ )
if self.instance and self.instance.file:
- self.fields['file'].required = False
+ self.fields["file"].required = False
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'staffdocument-form'
+ self.helper.form_id = "staffdocument-form"
self.helper.layout = Layout(
- Field('staff', type="hidden"),
- Field('name',),
- Field('description',),
- Field('file',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("staff", type="hidden"),
+ Field("name",),
+ Field("description",),
+ Field("file",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
class StaffProfileAdminForm(forms.ModelForm):
- """
- Form used when managing StaffProfile objects
- """
- first_name = forms.CharField(label=_('First Name'), required=True)
- last_name = forms.CharField(label=_('Last Name'), required=True)
- id_number = forms.CharField(label=_('ID Number'), required=True)
- nhif = forms.CharField(label=_('NHIF'), required=False)
- nssf = forms.CharField(label=_('NSSF'), required=False)
- pin_number = forms.CharField(label=_('PIN Number'), required=False)
+ """Form used when managing StaffProfile objects."""
+
+ first_name = forms.CharField(label=_("First Name"), required=True)
+ last_name = forms.CharField(label=_("Last Name"), required=True)
+ id_number = forms.CharField(label=_("ID Number"), required=True)
+ nhif = forms.CharField(label=_("NHIF"), required=False)
+ nssf = forms.CharField(label=_("NSSF"), required=False)
+ pin_number = forms.CharField(label=_("PIN Number"), required=False)
emergency_contact_name = forms.CharField(
- label=_('Emergency Contact Name'), required=False)
+ label=_("Emergency Contact Name"), required=False
+ )
emergency_contact_relationship = forms.CharField(
- label=_('Emergency Contact Relationship'), required=False)
+ label=_("Emergency Contact Relationship"), required=False
+ )
emergency_contact_number = PhoneNumberField(
- label=_('Emergency Contact Phone Number'), required=False)
+ label=_("Emergency Contact Phone Number"), required=False
+ )
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = StaffProfile
fields = [
- 'first_name',
- 'last_name',
- 'id_number',
- 'image',
- 'phone',
- 'sex',
- 'role',
- 'nhif',
- 'nssf',
- 'pin_number',
- 'address',
- 'birthday',
- 'leave_days',
- 'sick_days',
- 'overtime_allowed',
- 'start_date',
- 'end_date',
- 'emergency_contact_name',
- 'emergency_contact_number',
- 'emergency_contact_relationship'
+ "first_name",
+ "last_name",
+ "id_number",
+ "image",
+ "phone",
+ "sex",
+ "role",
+ "nhif",
+ "nssf",
+ "pin_number",
+ "address",
+ "birthday",
+ "leave_days",
+ "sick_days",
+ "overtime_allowed",
+ "start_date",
+ "end_date",
+ "emergency_contact_name",
+ "emergency_contact_number",
+ "emergency_contact_relationship",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
if self.instance and self.instance.image:
- self.fields['image'].required = False
+ self.fields["image"].required = False
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'staffprofile-form'
+ self.helper.form_id = "staffprofile-form"
self.helper.layout = Layout(
- Field('first_name',),
- Field('last_name',),
- Field('image',),
- Field('phone',),
- Field('id_number',),
- Field('sex',),
- Field('role',),
- Field('nhif',),
- Field('nssf',),
- Field('pin_number',),
- Field('address',),
- Field('birthday',),
- Field('leave_days',),
- Field('sick_days',),
- Field('overtime_allowed',),
- Field('start_date',),
- Field('end_date',),
- Field('emergency_contact_name',),
- Field('emergency_contact_number',),
- Field('emergency_contact_relationship',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("first_name",),
+ Field("last_name",),
+ Field("image",),
+ Field("phone",),
+ Field("id_number",),
+ Field("sex",),
+ Field("role",),
+ Field("nhif",),
+ Field("nssf",),
+ Field("pin_number",),
+ Field("address",),
+ Field("birthday",),
+ Field("leave_days",),
+ Field("sick_days",),
+ Field("overtime_allowed",),
+ Field("start_date",),
+ Field("end_date",),
+ Field("emergency_contact_name",),
+ Field("emergency_contact_number",),
+ Field("emergency_contact_relationship",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
def clean_id_number(self):
- """
- Check if id number is unique
- """
- value = self.cleaned_data.get('id_number')
+ """Check if id number is unique."""
+ value = self.cleaned_data.get("id_number")
# pylint: disable=no-member
- if StaffProfile.objects.exclude(
- id=self.instance.id).filter(data__id_number=value).exists():
- raise forms.ValidationError(
- _('This id number is already in use.'))
+ if (
+ StaffProfile.objects.exclude(id=self.instance.id)
+ .filter(data__id_number=value)
+ .exists()
+ ):
+ raise forms.ValidationError(_("This id number is already in use."))
return value
def clean_nssf(self):
- """
- Check if NSSF number is unique
- """
- value = self.cleaned_data.get('nssf')
+ """Check if NSSF number is unique."""
+ value = self.cleaned_data.get("nssf")
# pylint: disable=no-member
- if value and StaffProfile.objects.exclude(
- id=self.instance.id).filter(data__nssf=value).exists():
- raise forms.ValidationError(
- _('This NSSF number is already in use.'))
+ if (
+ value
+ and StaffProfile.objects.exclude(id=self.instance.id)
+ .filter(data__nssf=value)
+ .exists()
+ ):
+ raise forms.ValidationError(_("This NSSF number is already in use."))
return value
def clean_nhif(self):
- """
- Check if NHIF number is unique
- """
- value = self.cleaned_data.get('nhif')
+ """Check if NHIF number is unique."""
+ value = self.cleaned_data.get("nhif")
# pylint: disable=no-member
- if value and StaffProfile.objects.exclude(
- id=self.instance.id).filter(data__nhif=value).exists():
- raise forms.ValidationError(
- _('This NHIF number is already in use.'))
+ if (
+ value
+ and StaffProfile.objects.exclude(id=self.instance.id)
+ .filter(data__nhif=value)
+ .exists()
+ ):
+ raise forms.ValidationError(_("This NHIF number is already in use."))
return value
def clean_pin_number(self):
- """
- Check if PIN number is unique
- """
- value = self.cleaned_data.get('pin_number')
+ """Check if PIN number is unique."""
+ value = self.cleaned_data.get("pin_number")
# pylint: disable=no-member
- if value and StaffProfile.objects.exclude(
- id=self.instance.id).filter(data__pin_number=value).exists():
- raise forms.ValidationError(
- _('This PIN number is already in use.'))
+ if (
+ value
+ and StaffProfile.objects.exclude(id=self.instance.id)
+ .filter(data__pin_number=value)
+ .exists()
+ ):
+ raise forms.ValidationError(_("This PIN number is already in use."))
return value
def save(self, commit=True): # pylint: disable=unused-argument
- """
- Custom save method
- """
+ """Save the form."""
staffprofile = super().save()
- emergency_phone = self.cleaned_data.get('emergency_contact_number')
+ emergency_phone = self.cleaned_data.get("emergency_contact_number")
if isinstance(emergency_phone, PhoneNumber):
emergency_phone = emergency_phone.as_e164
json_data = {
- 'id_number': self.cleaned_data.get('id_number'),
- 'nhif': self.cleaned_data.get('nhif'),
- 'nssf': self.cleaned_data.get('nssf'),
- 'pin_number': self.cleaned_data.get('pin_number'),
- 'emergency_contact_name': self.cleaned_data.get(
- 'emergency_contact_name'),
- 'emergency_contact_relationship': self.cleaned_data.get(
- 'emergency_contact_relationship'),
- 'emergency_contact_number': emergency_phone,
+ "id_number": self.cleaned_data.get("id_number"),
+ "nhif": self.cleaned_data.get("nhif"),
+ "nssf": self.cleaned_data.get("nssf"),
+ "pin_number": self.cleaned_data.get("pin_number"),
+ "emergency_contact_name": self.cleaned_data.get("emergency_contact_name"),
+ "emergency_contact_relationship": self.cleaned_data.get(
+ "emergency_contact_relationship"
+ ),
+ "emergency_contact_number": emergency_phone,
}
staffprofile.data = json_data
staffprofile.save()
user = staffprofile.user
- user.first_name = self.cleaned_data['first_name']
- user.last_name = self.cleaned_data['last_name']
+ user.first_name = self.cleaned_data["first_name"]
+ user.last_name = self.cleaned_data["last_name"]
user.save()
return staffprofile
class StaffProfileAdminCreateForm(StaffProfileAdminForm):
- """
- Form used when creating new Staff Profiles
- """
+ """Form used when creating new Staff Profiles."""
+
user = forms.ModelChoiceField(
- label=_('User'), queryset=User.objects.filter(staffprofile=None))
+ label=_("User"), queryset=User.objects.filter(staffprofile=None)
+ )
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = StaffProfile
fields = [
- 'user',
- 'first_name',
- 'last_name',
- 'id_number',
- 'image',
- 'phone',
- 'sex',
- 'role',
- 'nhif',
- 'nssf',
- 'pin_number',
- 'address',
- 'birthday',
- 'leave_days',
- 'sick_days',
- 'overtime_allowed',
- 'start_date',
- 'end_date',
- 'emergency_contact_name',
- 'emergency_contact_number',
- 'emergency_contact_relationship'
+ "user",
+ "first_name",
+ "last_name",
+ "id_number",
+ "image",
+ "phone",
+ "sex",
+ "role",
+ "nhif",
+ "nssf",
+ "pin_number",
+ "address",
+ "birthday",
+ "leave_days",
+ "sick_days",
+ "overtime_allowed",
+ "start_date",
+ "end_date",
+ "emergency_contact_name",
+ "emergency_contact_number",
+ "emergency_contact_relationship",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
if self.instance and self.instance.image:
- self.fields['image'].required = False
+ self.fields["image"].required = False
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'staffprofile-form'
+ self.helper.form_id = "staffprofile-form"
self.helper.layout = Layout(
- Field('user',),
- Field('first_name',),
- Field('last_name',),
- Field('image',),
- Field('phone',),
- Field('id_number',),
- Field('sex',),
- Field('role',),
- Field('nhif',),
- Field('nssf',),
- Field('pin_number',),
- Field('address',),
- Field('birthday',),
- Field('leave_days',),
- Field('sick_days',),
- Field('overtime_allowed',),
- Field('start_date',),
- Field('end_date',),
- Field('emergency_contact_name',),
- Field('emergency_contact_number',),
- Field('emergency_contact_relationship',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("user",),
+ Field("first_name",),
+ Field("last_name",),
+ Field("image",),
+ Field("phone",),
+ Field("id_number",),
+ Field("sex",),
+ Field("role",),
+ Field("nhif",),
+ Field("nssf",),
+ Field("pin_number",),
+ Field("address",),
+ Field("birthday",),
+ Field("leave_days",),
+ Field("sick_days",),
+ Field("overtime_allowed",),
+ Field("start_date",),
+ Field("end_date",),
+ Field("emergency_contact_name",),
+ Field("emergency_contact_number",),
+ Field("emergency_contact_relationship",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
class StaffProfileUserForm(StaffProfileAdminForm):
- """
- Form used when the user is updating their own data
- """
+ """Form used when the user is updating their own data."""
class Meta: # pylint: disable=too-few-public-methods
- """
- Class meta options
- """
+ """Class meta options."""
+
model = StaffProfile
fields = [
- 'first_name',
- 'last_name',
- 'id_number',
- 'image',
- 'phone',
- 'sex',
- 'nhif',
- 'nssf',
- 'pin_number',
- 'address',
- 'birthday',
- 'emergency_contact_name',
- 'emergency_contact_number',
- 'emergency_contact_relationship'
+ "first_name",
+ "last_name",
+ "id_number",
+ "image",
+ "phone",
+ "sex",
+ "nhif",
+ "nssf",
+ "pin_number",
+ "address",
+ "birthday",
+ "emergency_contact_name",
+ "emergency_contact_number",
+ "emergency_contact_relationship",
]
def __init__(self, *args, **kwargs):
- self.request = kwargs.pop('request', None)
+ """Initialize the form."""
+ self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
- self.helper.form_method = 'post'
+ self.helper.form_method = "post"
self.helper.render_required_fields = True
self.helper.form_show_labels = True
self.helper.html5_required = True
- self.helper.form_id = 'staffprofile-user-form'
+ self.helper.form_id = "staffprofile-user-form"
self.helper.layout = Layout(
- Field('first_name',),
- Field('last_name',),
- Field('image',),
- Field('phone',),
- Field('id_number',),
- Field('sex',),
- Field('nhif',),
- Field('nssf',),
- Field('pin_number',),
- Field('address',),
- Field('birthday',),
- Field('emergency_contact_name',),
- Field('emergency_contact_number',),
- Field('emergency_contact_relationship',),
- FormActions(
- Submit('submitBtn', _('Submit'), css_class='btn-primary'),
- )
+ Field("first_name",),
+ Field("last_name",),
+ Field("image",),
+ Field("phone",),
+ Field("id_number",),
+ Field("sex",),
+ Field("nhif",),
+ Field("nssf",),
+ Field("pin_number",),
+ Field("address",),
+ Field("birthday",),
+ Field("emergency_contact_name",),
+ Field("emergency_contact_number",),
+ Field("emergency_contact_relationship",),
+ FormActions(Submit("submitBtn", _("Submit"), css_class="btn-primary"),),
)
diff --git a/small_small_hr/migrations/0008_auto_20200607_1537.py b/small_small_hr/migrations/0008_auto_20200607_1537.py
new file mode 100644
index 0000000..dd9f1bd
--- /dev/null
+++ b/small_small_hr/migrations/0008_auto_20200607_1537.py
@@ -0,0 +1,65 @@
+# Generated by Django 3.0.7 on 2020-06-07 12:37
+# pylint: disable=invalid-name,missing-module-docstring,missing-class-docstring
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("small_small_hr", "0007_auto_20190625_2111"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="leave", old_name="reason", new_name="review_reason",
+ ),
+ migrations.RenameField(
+ model_name="leave", old_name="status", new_name="review_status",
+ ),
+ migrations.RenameField(
+ model_name="overtime", old_name="reason", new_name="review_reason",
+ ),
+ migrations.RenameField(
+ model_name="overtime", old_name="status", new_name="review_status",
+ ),
+ migrations.RemoveField(model_name="leave", name="comments",),
+ migrations.RemoveField(model_name="overtime", name="comments",),
+ migrations.AddField(
+ model_name="leave",
+ name="review_date",
+ field=models.DateTimeField(
+ blank=True, default=None, null=True, verbose_name="Review Date"
+ ),
+ ),
+ migrations.AddField(
+ model_name="overtime",
+ name="review_date",
+ field=models.DateTimeField(
+ blank=True, default=None, null=True, verbose_name="Review Date"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="annualleave",
+ name="year",
+ field=models.PositiveIntegerField(
+ choices=[
+ (2017, 2017),
+ (2018, 2018),
+ (2019, 2019),
+ (2020, 2020),
+ (2021, 2021),
+ (2022, 2022),
+ (2023, 2023),
+ (2024, 2024),
+ (2025, 2025),
+ (2026, 2026),
+ (2027, 2027),
+ (2028, 2028),
+ (2029, 2029),
+ ],
+ db_index=True,
+ default=2017,
+ verbose_name="Year",
+ ),
+ ),
+ ]
diff --git a/small_small_hr/migrations/0009_auto_20200607_2244.py b/small_small_hr/migrations/0009_auto_20200607_2244.py
new file mode 100644
index 0000000..14b0e95
--- /dev/null
+++ b/small_small_hr/migrations/0009_auto_20200607_2244.py
@@ -0,0 +1,52 @@
+# Generated by Django 3.0.7 on 2020-06-07 19:44
+# pylint: disable=invalid-name,missing-module-docstring,missing-class-docstring
+import django.db.models.deletion
+from django.db import migrations, models
+
+import mptt.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("small_small_hr", "0008_auto_20200607_1537"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="staffprofile",
+ name="level",
+ field=models.PositiveIntegerField(default=1, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="staffprofile",
+ name="lft",
+ field=models.PositiveIntegerField(default=1, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="staffprofile",
+ name="rght",
+ field=models.PositiveIntegerField(default=1, editable=False),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="staffprofile",
+ name="supervisor",
+ field=mptt.fields.TreeForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="children",
+ to="small_small_hr.StaffProfile",
+ verbose_name="Supervisor",
+ ),
+ ),
+ migrations.AddField(
+ model_name="staffprofile",
+ name="tree_id",
+ field=models.PositiveIntegerField(db_index=True, default=1, editable=False),
+ preserve_default=False,
+ ),
+ ]
diff --git a/small_small_hr/models.py b/small_small_hr/models.py
index ce1ab7d..17fbade 100644
--- a/small_small_hr/models.py
+++ b/small_small_hr/models.py
@@ -1,20 +1,23 @@
-"""
-Models module for small_small_hr
-"""
+"""Models module for small_small_hr."""
from datetime import datetime, timedelta
from decimal import Decimal
+from typing import Optional
from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models import Q
from django.utils import timezone
+from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
+from model_reviews.models import AbstractReview
+from mptt.models import MPTTModel, TreeForeignKey
from phonenumber_field.modelfields import PhoneNumberField
from private_storage.fields import PrivateFileField
from sorl.thumbnail import ImageField
+from small_small_hr.constants import EMAIL_TEMPLATE_PATH
from small_small_hr.managers import LeaveManager
USER = settings.AUTH_USER_MODEL
@@ -22,346 +25,424 @@
class TimeStampedModel(models.Model):
- """
- Abstract model class that includes timestamp fields
- """
- created = models.DateTimeField(
- verbose_name=_('Created'),
- auto_now_add=True)
- modified = models.DateTimeField(
- verbose_name=_('Modified'),
- auto_now=True)
+ """Abstract model class that includes timestamp fields."""
+
+ created = models.DateTimeField(verbose_name=_("Created"), auto_now_add=True)
+ modified = models.DateTimeField(verbose_name=_("Modified"), auto_now=True)
# pylint: disable=too-few-public-methods
class Meta:
- """
- Meta options for TimeStampedModel
- """
+ """Meta options for TimeStampedModel."""
+
abstract = True
class Role(TimeStampedModel, models.Model):
- """
- Model class for staff member role
- """
- name = models.CharField(_('Name'), max_length=255)
- description = models.TextField(_('Description'), blank=True, default='')
+ """Model class for staff member role."""
+
+ name = models.CharField(_("Name"), max_length=255)
+ description = models.TextField(_("Description"), blank=True, default="")
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for StaffDocument
- """
+ """Meta options for StaffDocument."""
+
abstract = False
- verbose_name = _('Role')
- verbose_name_plural = _('Roles')
- ordering = ['name', 'created']
+ verbose_name = _("Role")
+ verbose_name_plural = _("Roles")
+ ordering = ["name", "created"]
def __str__(self):
+ """Unicode representation of class object."""
# pylint: disable=no-member
return self.name
-class StaffProfile(TimeStampedModel, models.Model):
+class StaffProfile(TimeStampedModel, MPTTModel):
"""
- StaffProfile model class
+ StaffProfile model class.
+
Extends auth.User and adds more fields
"""
# sex choices
# according to https://en.wikipedia.org/wiki/ISO/IEC_5218
- NOT_KNOWN = '0'
- MALE = '1'
- FEMALE = '2'
- NOT_APPLICABLE = '9'
+ NOT_KNOWN = "0"
+ MALE = "1"
+ FEMALE = "2"
+ NOT_APPLICABLE = "9"
SEX_CHOICES = (
- (NOT_KNOWN, _('Not Known')),
- (MALE, _('Male')),
- (FEMALE, _('Female')),
- (NOT_APPLICABLE, _('Not Applicable'))
+ (NOT_KNOWN, _("Not Known")),
+ (MALE, _("Male")),
+ (FEMALE, _("Female")),
+ (NOT_APPLICABLE, _("Not Applicable")),
)
- user = models.OneToOneField(
- USER, verbose_name=_('User'), on_delete=models.CASCADE)
- image = ImageField(upload_to="staff-images/", max_length=255,
- verbose_name=_("Profile Image"),
- help_text=_("A square image works best"), blank=True)
- sex = models.CharField(_('Gender'), choices=SEX_CHOICES, max_length=1,
- default=NOT_KNOWN, blank=True, db_index=True)
- role = models.ForeignKey(Role, verbose_name=_('Role'), blank=True,
- default=None, null=True,
- on_delete=models.SET_NULL)
- phone = PhoneNumberField(_('Phone'), blank=True, default='')
- address = models.TextField(_('Addresss'), blank=True, default="")
- birthday = models.DateField(_('Birthday'), blank=True, default=None,
- null=True)
+ user = models.OneToOneField(USER, verbose_name=_("User"), on_delete=models.CASCADE)
+ supervisor = TreeForeignKey(
+ "self",
+ verbose_name=_("Manager"),
+ on_delete=models.PROTECT,
+ null=True,
+ blank=True,
+ related_name="children",
+ )
+ image = ImageField(
+ upload_to="staff-images/",
+ max_length=255,
+ verbose_name=_("Profile Image"),
+ help_text=_("A square image works best"),
+ blank=True,
+ )
+ sex = models.CharField(
+ _("Gender"),
+ choices=SEX_CHOICES,
+ max_length=1,
+ default=NOT_KNOWN,
+ blank=True,
+ db_index=True,
+ )
+ role = models.ForeignKey(
+ Role,
+ verbose_name=_("Role"),
+ blank=True,
+ default=None,
+ null=True,
+ on_delete=models.SET_NULL,
+ )
+ phone = PhoneNumberField(_("Phone"), blank=True, default="")
+ address = models.TextField(_("Addresss"), blank=True, default="")
+ birthday = models.DateField(_("Birthday"), blank=True, default=None, null=True)
leave_days = models.PositiveIntegerField(
- _('Leave days'), default=21, blank=True,
- help_text=_('Number of leave days allowed in a year.'))
+ _("Leave days"),
+ default=21,
+ blank=True,
+ help_text=_("Number of leave days allowed in a year."),
+ )
sick_days = models.PositiveIntegerField(
- _('Sick days'), default=10, blank=True,
- help_text=_('Number of sick days allowed in a year.'))
+ _("Sick days"),
+ default=10,
+ blank=True,
+ help_text=_("Number of sick days allowed in a year."),
+ )
overtime_allowed = models.BooleanField(
- _('Overtime allowed'), blank=True, default=False)
+ _("Overtime allowed"), blank=True, default=False
+ )
start_date = models.DateField(
- _('Start Date'), null=True, default=None, blank=True,
- help_text=_('The start date of employment'))
+ _("Start Date"),
+ null=True,
+ default=None,
+ blank=True,
+ help_text=_("The start date of employment"),
+ )
end_date = models.DateField(
- _('End Date'), null=True, default=None, blank=True,
- help_text=_('The end date of employment'))
- data = JSONField(_('Data'), default=dict, blank=True)
+ _("End Date"),
+ null=True,
+ default=None,
+ blank=True,
+ help_text=_("The end date of employment"),
+ )
+ data = JSONField(_("Data"), default=dict, blank=True)
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for StaffProfile
- """
+ """Meta options for StaffProfile."""
+
abstract = False
- verbose_name = _('Staff Profile')
- verbose_name_plural = _('Staff Profiles')
- ordering = ['user__first_name', 'user__last_name', 'user__username',
- 'created']
+ verbose_name = _("Staff Profile")
+ verbose_name_plural = _("Staff Profiles")
+ ordering = ["user__first_name", "user__last_name", "user__username", "created"]
+
+ class MPTTMeta:
+ """Meta options for MPTT."""
+
+ parent_attr = "supervisor"
def get_name(self):
- """
- Returns the staff member's name
- """
+ """Return the staff member's name."""
# pylint: disable=no-member
- return f'{self.user.first_name} {self.user.last_name}'
+ return f"{self.user.first_name} {self.user.last_name}"
- def get_approved_leave_days(self, year: int = datetime.today().year):
- """
- Get approved leave days in the current year
- """
+ @cached_property
+ def _current_year(self): # pylint: disable=no-self-use
+ """Get the current year."""
+ return datetime.today().year
+
+ def get_approved_leave_days(self, year: Optional[int] = None):
+ """Get approved leave days in the current year."""
# pylint: disable=no-member
return get_taken_leave_days(
staffprofile=self,
status=Leave.APPROVED,
leave_type=Leave.REGULAR,
- start_year=year,
- end_year=year
+ start_year=year or self._current_year,
+ end_year=year or self._current_year,
)
- def get_approved_sick_days(self, year: int = datetime.today().year):
- """
- Get approved leave days in the current year
- """
+ def get_approved_sick_days(self, year: Optional[int] = None):
+ """Get approved leave days in the current year."""
return get_taken_leave_days(
staffprofile=self,
status=Leave.APPROVED,
leave_type=Leave.SICK,
- start_year=year,
- end_year=year
+ start_year=year or self._current_year,
+ end_year=year or self._current_year,
)
- def get_available_leave_days(self, year: int = datetime.today().year):
- """
- Get available leave days
- """
+ def get_available_leave_days(self, year: Optional[int] = None):
+ """Get available leave days."""
try:
# pylint: disable=no-member
leave_record = AnnualLeave.objects.get(
- leave_type=Leave.REGULAR,
- staff=self,
- year=year)
+ leave_type=Leave.REGULAR, staff=self, year=year or self._current_year
+ )
except AnnualLeave.DoesNotExist:
return Decimal(0)
else:
return leave_record.get_available_leave_days()
- def get_available_sick_days(self, year: int = datetime.today().year):
- """
- Get available sick days
- """
+ def get_available_sick_days(self, year: Optional[int] = None):
+ """Get available sick days."""
try:
# pylint: disable=no-member
leave_record = AnnualLeave.objects.get(
- leave_type=Leave.SICK,
- staff=self,
- year=year)
+ leave_type=Leave.SICK, staff=self, year=year or self._current_year
+ )
except AnnualLeave.DoesNotExist:
return Decimal(0)
else:
return leave_record.get_available_leave_days()
def __str__(self):
+ """Unicode representation of class object."""
return self.get_name() # pylint: disable=no-member
class StaffDocument(TimeStampedModel, models.Model):
- """
- StaffDocument model class
- """
+ """StaffDocument model class."""
+
staff = models.ForeignKey(
- StaffProfile, verbose_name=_('Staff Member'), on_delete=models.CASCADE)
- name = models.CharField(_('Name'), max_length=255)
- description = models.TextField(_('Description'), blank=True, default='')
+ StaffProfile, verbose_name=_("Staff Member"), on_delete=models.CASCADE
+ )
+ name = models.CharField(_("Name"), max_length=255)
+ description = models.TextField(_("Description"), blank=True, default="")
file = PrivateFileField(
- _('File'), upload_to='staff-documents/',
+ _("File"),
+ upload_to="staff-documents/",
help_text=_("Upload staff member document"),
content_types=[
- 'application/pdf',
- 'application/msword',
- 'application/vnd.oasis.opendocument.text',
- 'image/jpeg',
- 'image/png'
+ "application/pdf",
+ "application/msword",
+ "application/vnd.oasis.opendocument.text",
+ "image/jpeg",
+ "image/png",
],
- max_file_size=10485760
+ max_file_size=10485760,
)
public = models.BooleanField(
- _('Public'),
- help_text=_('If public, it will be available to everyone.'),
- blank=True, default=False)
+ _("Public"),
+ help_text=_("If public, it will be available to everyone."),
+ blank=True,
+ default=False,
+ )
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for StaffDocument
- """
+ """Meta options for StaffDocument."""
+
abstract = False
- verbose_name = _('Staff Document')
- verbose_name_plural = _('Staff Documents')
- ordering = ['staff', 'name', '-created']
+ verbose_name = _("Staff Document")
+ verbose_name_plural = _("Staff Documents")
+ ordering = ["staff", "name", "-created"]
def __str__(self):
- # pylint: disable=no-member
- return f'{self.staff.get_name()} - {self.name}'
+ """Unicode representation of class object."""
+ return f"{self.staff.get_name()} - {self.name}"
-class BaseStaffRequest(TimeStampedModel, models.Model):
- """
- Abstract model class for Leave & Overtime tracking
- """
- APPROVED = '1'
- REJECTED = '2'
- PENDING = '3'
-
- STATUS_CHOICES = (
- (APPROVED, _('Approved')),
- (PENDING, _('Pending')),
- (REJECTED, _('Rejected'))
- )
+class BaseStaffRequest(TimeStampedModel, AbstractReview):
+ """Abstract model class for Leave & Overtime tracking."""
staff = models.ForeignKey(
- StaffProfile, verbose_name=_('Staff Member'), on_delete=models.CASCADE)
- start = models.DateTimeField(_('Start Date'))
- end = models.DateTimeField(_('End Date'))
- reason = models.TextField(_('Reason'), blank=True, default='')
- status = models.CharField(
- _('Status'), max_length=1, choices=STATUS_CHOICES, default=PENDING,
- blank=True, db_index=True)
- comments = models.TextField(_('Comments'), blank=True, default='')
+ StaffProfile, verbose_name=_("Staff Member"), on_delete=models.CASCADE
+ )
+ start = models.DateTimeField(_("Start Date"))
+ end = models.DateTimeField(_("End Date"))
+ review_reason = models.TextField(_("Reason"), blank=True, default="")
+ review_status = models.CharField(
+ _("Status"),
+ max_length=1,
+ choices=AbstractReview.STATUS_CHOICES,
+ default=AbstractReview.PENDING,
+ blank=True,
+ db_index=True,
+ )
+
+ # MODEL REVIEW OPTIONS
+ # path to function that will be used to determine reviewers
+ set_reviewers_function: Optional[
+ str
+ ] = "small_small_hr.reviews.set_staff_request_reviewer"
+ # path to function that will be used to determine the user for a review object
+ set_user_function: Optional[
+ str
+ ] = "small_small_hr.reviews.set_staff_request_review_user"
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for StaffDocument
- """
+ """Meta options for BaseStaffRequest."""
+
abstract = True
class Leave(BaseStaffRequest):
- """
- Leave model class
- """
- SICK = '1'
- REGULAR = '2'
+ """Leave model class."""
+
+ SICK = "1"
+ REGULAR = "2"
TYPE_CHOICES = (
- (SICK, _('Sick Leave')),
- (REGULAR, _('Regular Leave')),
+ (SICK, _("Sick Leave")),
+ (REGULAR, _("Regular Leave")),
)
leave_type = models.CharField(
- _('Type'), max_length=1, choices=TYPE_CHOICES, default=REGULAR,
- blank=True, db_index=True)
+ _("Type"),
+ max_length=1,
+ choices=TYPE_CHOICES,
+ default=REGULAR,
+ blank=True,
+ db_index=True,
+ )
objects = LeaveManager()
+ # MODEL REVIEW OPTIONS
+ email_template_path = EMAIL_TEMPLATE_PATH
+ # path to function that will be used to send email to reviewers
+ request_for_review_function: Optional[
+ str
+ ] = "small_small_hr.emails.send_request_for_leave_review"
+ # path to function that will be used to send email to user after review
+ review_complete_notify_function: Optional[
+ str
+ ] = "small_small_hr.emails.send_leave_review_complete_notice"
+
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for Leave
- """
+ """Meta options for Leave."""
+
abstract = False
- verbose_name = _('Leave')
- verbose_name_plural = _('Leave')
- ordering = ['staff', '-start']
+ verbose_name = _("Leave")
+ verbose_name_plural = _("Leave")
+ ordering = ["staff", "-start"]
def __str__(self):
+ """Unicode representation of class object."""
# pylint: disable=no-member
- return _(f'{self.staff.get_name()}: {self.start} to {self.end}')
+ return _(f"{self.staff.get_name()}: {self.start} to {self.end}")
+
+ def get_duration(self):
+ """Get duration."""
+ return self.end - self.start
+
+ @cached_property
+ def duration(self):
+ """Get duration as a property."""
+ return self.get_duration()
class OverTime(BaseStaffRequest):
- """
- Overtime model class
- """
+ """Overtime model class."""
+
date = models.DateField(
- _('Date'), auto_now=False, auto_now_add=False, db_index=True)
- start = models.TimeField(_('Start'), auto_now=False, auto_now_add=False)
- end = models.TimeField(_('End'), auto_now=False, auto_now_add=False)
+ _("Date"), auto_now=False, auto_now_add=False, db_index=True
+ )
+ start = models.TimeField(_("Start"), auto_now=False, auto_now_add=False)
+ end = models.TimeField(_("End"), auto_now=False, auto_now_add=False)
+
+ # MODEL REVIEW OPTIONS
+ email_template_path = EMAIL_TEMPLATE_PATH
+ # path to function that will be used to send email to reviewers
+ request_for_review_function: Optional[
+ str
+ ] = "small_small_hr.emails.send_request_for_overtime_review"
+ # path to function that will be used to send email to user after review
+ review_complete_notify_function: Optional[
+ str
+ ] = "small_small_hr.emails.send_overtime_review_complete_notice"
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for OverTime
- """
+ """Meta options for OverTime."""
+
abstract = False
- verbose_name = _('Overtime')
- verbose_name_plural = _('Overtime')
- ordering = ['staff', '-date', 'start']
+ verbose_name = _("Overtime")
+ verbose_name_plural = _("Overtime")
+ ordering = ["staff", "-date", "start"]
def __str__(self):
+ """Unicode representation of class object."""
name = self.staff.get_name() # pylint: disable=no-member
- return _(f'{name}: {self.date} from {self.start} to {self.end}')
+ return _(f"{name}: {self.date} from {self.start} to {self.end}")
def get_duration(self):
- """
- Get duration
- """
+ """Get duration."""
start = datetime.combine(self.date, self.start)
end = datetime.combine(self.date, self.end)
return end - start
+ @cached_property
+ def duration(self):
+ """Get duration as a property."""
+ return self.get_duration()
+
class AnnualLeave(TimeStampedModel, models.Model):
"""
- Model to keep track of staff employee annual leave
+ Model to keep track of staff employee annual leave.
This model is meant to be populated once a year
Each staff member can only have one record per leave_type per year
"""
- YEAR_CHOICES = [
- (r, r) for r in range(2017, datetime.today().year + 10)
- ]
+
+ YEAR_CHOICES = [(r, r) for r in range(2017, datetime.today().year + 10)]
year = models.PositiveIntegerField(
- _('Year'), choices=YEAR_CHOICES, default=2017, db_index=True)
+ _("Year"), choices=YEAR_CHOICES, default=2017, db_index=True
+ )
staff = models.ForeignKey(
- StaffProfile, verbose_name=_('Staff Member'), on_delete=models.CASCADE)
+ StaffProfile, verbose_name=_("Staff Member"), on_delete=models.CASCADE
+ )
leave_type = models.CharField(
- _('Type'), max_length=1, choices=Leave.TYPE_CHOICES, db_index=True)
+ _("Type"), max_length=1, choices=Leave.TYPE_CHOICES, db_index=True
+ )
allowed_days = models.PositiveIntegerField(
- _('Allowed Leave days'), default=21, blank=True,
- help_text=_('Number of leave days allowed in a year.'))
+ _("Allowed Leave days"),
+ default=21,
+ blank=True,
+ help_text=_("Number of leave days allowed in a year."),
+ )
carried_over_days = models.PositiveIntegerField(
- _('Carried Over Leave days'), default=0, blank=True,
- help_text=_('Number of leave days carried over into this year.'))
+ _("Carried Over Leave days"),
+ default=0,
+ blank=True,
+ help_text=_("Number of leave days carried over into this year."),
+ )
class Meta: # pylint: disable=too-few-public-methods
- """
- Meta options for AnnualLeave
- """
- verbose_name = _('Annual Leave')
- verbose_name_plural = _('Annual Leave')
- ordering = ['-year', 'leave_type', 'staff']
- unique_together = (('year', 'staff', 'leave_type'),)
+ """Meta options for AnnualLeave."""
+
+ verbose_name = _("Annual Leave")
+ verbose_name_plural = _("Annual Leave")
+ ordering = ["-year", "leave_type", "staff"]
+ unique_together = (("year", "staff", "leave_type"),)
def __str__(self):
+ """Unicode representation of class object."""
# pylint: disable=no-member
return _(
- f'{self.year}: {self.staff.get_name()} '
- f'{self.get_leave_type_display()}')
+ f"{self.year}: {self.staff.get_name()} " f"{self.get_leave_type_display()}"
+ )
def get_cumulative_leave_taken(self):
"""
- Get the cumulative leave taken
+ Get the cumulative leave taken.
Returns a timedelta
"""
@@ -370,13 +451,11 @@ def get_cumulative_leave_taken(self):
status=Leave.APPROVED,
leave_type=self.leave_type,
start_year=self.year,
- end_year=self.year
+ end_year=self.year,
)
def get_available_leave_days(self, month: int = 12):
- """
- Get the remaining leave days
- """
+ """Get the remaining leave days."""
if month <= 0:
month = 1
elif month > 12:
@@ -402,24 +481,24 @@ def get_available_leave_days(self, month: int = 12):
class FreeDay(models.Model):
"""Model definition for FreeDay."""
+
name = models.CharField(_("Name"), max_length=255)
- date = models.DateField(_('Date'), unique=True)
+ date = models.DateField(_("Date"), unique=True)
class Meta:
"""Meta definition for FreeDay."""
- ordering = ['-date']
- verbose_name = _('Free Day')
- verbose_name_plural = _('Free Days')
+
+ ordering = ["-date"]
+ verbose_name = _("Free Day")
+ verbose_name_plural = _("Free Days")
def __str__(self):
- """Unicode representation of FreeDay."""
+ """Unicode representation of class object."""
return f"{self.date.year} - {self.name}"
def get_days(start: object, end: object):
- """
- Yield the days between two datetime objects
- """
+ """Yield the days between two datetime objects."""
current_tz = timezone.get_current_timezone()
local_start = current_tz.normalize(start)
local_end = current_tz.normalize(end)
@@ -428,30 +507,25 @@ def get_days(start: object, end: object):
yield local_start.date() + timedelta(days=i)
-def get_taken_leave_days(
- staffprofile: object,
- status: str,
- leave_type: str,
- start_year: int,
- end_year: int):
+def get_taken_leave_days( # pylint: disable=bad-continuation
+ staffprofile: object, status: str, leave_type: str, start_year: int, end_year: int
+):
"""
- Calculate the number of leave days actually taken,
- taking into account weekends and weekend policy
+ Calculate the number of leave days actually taken.
+
+ Takes into account weekends and weekend policy
"""
count = Decimal(0)
free_days = FreeDay.objects.filter(
date__year__gte=start_year, date__year__lte=end_year
- ).values_list('date', flat=True)
+ ).values_list("date", flat=True)
queryset = Leave.objects.filter(
- staff=staffprofile,
- status=status,
- leave_type=leave_type).filter(
- Q(start__year__gte=start_year) | Q(end__year__lte=end_year))
+ staff=staffprofile, review_status=status, leave_type=leave_type
+ ).filter(Q(start__year__gte=start_year) | Q(end__year__lte=end_year))
for leave_obj in queryset:
days = get_days(start=leave_obj.start, end=leave_obj.end)
for day in days:
- if day.year >= start_year and day.year <= end_year and\
- day not in free_days:
+ if day.year >= start_year and day.year <= end_year and day not in free_days:
day_value = settings.SSHR_DAY_LEAVE_VALUES[day.isoweekday()]
count = count + Decimal(day_value)
return count
diff --git a/small_small_hr/reviews.py b/small_small_hr/reviews.py
new file mode 100644
index 0000000..c4e2f70
--- /dev/null
+++ b/small_small_hr/reviews.py
@@ -0,0 +1,50 @@
+"""Review module for small-small-hr."""
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.db import models
+
+from model_reviews.models import Reviewer
+
+from small_small_hr.constants import STAFF
+
+
+def set_staff_request_review_user(review_obj: models.Model):
+ """
+ Set user for Leave and Overtime requests.
+
+ This is the default strategy of auto-setting the user for a review object.
+ It simply sets the user using a field on the model object that is under review.
+ """
+ if not review_obj.user:
+ object_under_review = review_obj.content_object
+ staff_profile = getattr(object_under_review, STAFF, None)
+ if staff_profile:
+ review_obj.user = staff_profile.user
+
+
+def set_staff_request_reviewer(review_obj: models.Model):
+ """
+ Set reviewer for Leave and Overtime requests.
+
+ This is the strategy that will be used:
+
+ 1. Set it to the staff member's supervisor
+ 2. Additionally, set to all members of the Group named SSHR_ADMIN_USER_GROUP_NAME
+ """
+ if review_obj.user:
+ staff_member = review_obj.user.staffprofile
+ manager = staff_member.supervisor
+ if (
+ manager
+ and not Reviewer.objects.filter(
+ review=review_obj, user=manager.user
+ ).exists()
+ ):
+ reviewer = Reviewer(review=review_obj, user=manager.user)
+ reviewer.save() # ensure save method is called
+
+ hr_group_name = settings.SSHR_ADMIN_USER_GROUP_NAME
+ for user in User.objects.filter(groups__name=hr_group_name):
+ if not Reviewer.objects.filter(review=review_obj, user=user).exists():
+ reviewer = Reviewer(review=review_obj, user=user)
+ reviewer.save() # ensure save method is called
diff --git a/small_small_hr/settings.py b/small_small_hr/settings.py
index 02546a0..18a34e1 100644
--- a/small_small_hr/settings.py
+++ b/small_small_hr/settings.py
@@ -16,14 +16,16 @@
SSHR_ALLOW_OVERSUBSCRIBE = True # allow taking more leave days one has
SSHR_DEFAULT_TIME = 7 # default time of the day for leave
SSHR_FREE_DAYS = [
- {'day': 1, 'month': 1}, # New year
- {'day': 1, 'month': 5}, # labour day
- {'day': 1, 'month': 6}, # Madaraka day
- {'day': 20, 'month': 10}, # Mashujaa day
- {'day': 12, 'month': 12}, # Jamhuri day
- {'day': 25, 'month': 12}, # Christmas
- {'day': 26, 'month': 12}, # Boxing day
+ {"day": 1, "month": 1}, # New year
+ {"day": 1, "month": 5}, # labour day
+ {"day": 1, "month": 6}, # Madaraka day
+ {"day": 20, "month": 10}, # Mashujaa day
+ {"day": 12, "month": 12}, # Jamhuri day
+ {"day": 25, "month": 12}, # Christmas
+ {"day": 26, "month": 12}, # Boxing day
] # these are days that are not counted when getting taken leave days
+# admins
+SSHR_ADMIN_USER_GROUP_NAME = "Human Resource"
# emails
SSHR_ADMIN_NAME = "HR"
SSHR_ADMIN_EMAILS = [settings.DEFAULT_FROM_EMAIL]
diff --git a/small_small_hr/templates/small_small_hr/email/generic_email_body.html b/small_small_hr/templates/small_small_hr/email/generic_email_body.html
deleted file mode 100644
index 7fed0ef..0000000
--- a/small_small_hr/templates/small_small_hr/email/generic_email_body.html
+++ /dev/null
@@ -1,7 +0,0 @@
-Hello {{name}},
-{{message|linebreaks}}
-
-Thank you,
-{{SITE.name}}
-------
-http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/generic_email_body.txt b/small_small_hr/templates/small_small_hr/email/generic_email_body.txt
deleted file mode 100644
index ac06063..0000000
--- a/small_small_hr/templates/small_small_hr/email/generic_email_body.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-Hello {{name}},
-
-{{message}}
-
-Thank you,
-
-{{SITE.name}}
-------
-http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/generic_email_subject.txt b/small_small_hr/templates/small_small_hr/email/generic_email_subject.txt
deleted file mode 100644
index b975387..0000000
--- a/small_small_hr/templates/small_small_hr/email/generic_email_subject.txt
+++ /dev/null
@@ -1 +0,0 @@
-{{subject}}
\ No newline at end of file
diff --git a/small_small_hr/templates/small_small_hr/email/leave_application_email_body.html b/small_small_hr/templates/small_small_hr/email/leave_application_email_body.html
index f0d3095..2dd511d 100644
--- a/small_small_hr/templates/small_small_hr/email/leave_application_email_body.html
+++ b/small_small_hr/templates/small_small_hr/email/leave_application_email_body.html
@@ -1,5 +1,8 @@
-Hello,
-{{message|linebreaks}}
+{{ object.content_object.staff.get_name }} requested time off:
+{{ object.content_object.duration.days }} days of {{ object.content_object.get_leave_type_display}}
+{{ object.content_object.start|date:"D, d M Y" }} - {{ object.content_object.end|date:"D, d M Y" }}
+Available Balance: {{ object.content_object.staff.get_available_leave_days|floatformat:2 }} days
+Please log in to process the above: http://{{SITE.name}}/reviews/{{ object.pk }}
Thank you,
{{SITE.name}}
diff --git a/small_small_hr/templates/small_small_hr/email/leave_application_email_body.txt b/small_small_hr/templates/small_small_hr/email/leave_application_email_body.txt
index a1e77af..ec10080 100644
--- a/small_small_hr/templates/small_small_hr/email/leave_application_email_body.txt
+++ b/small_small_hr/templates/small_small_hr/email/leave_application_email_body.txt
@@ -1,9 +1,14 @@
-Hello,
+{{ object.content_object.staff.get_name }} requested time off:
-{{message}}
+{{ object.content_object.duration.days }} days of {{ object.content_object.get_leave_type_display}}
+{{ object.content_object.start|date:"D, d M Y" }} - {{ object.content_object.end|date:"D, d M Y" }}
+Available Balance: {{ object.content_object.staff.get_available_leave_days|floatformat:2 }} days
+
+Please log in to process the above: http://{{SITE.name}}/reviews/{{ object.pk }}
Thank you,
+
{{SITE.name}}
------
http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/leave_application_email_subject.txt b/small_small_hr/templates/small_small_hr/email/leave_application_email_subject.txt
index b975387..019041e 100644
--- a/small_small_hr/templates/small_small_hr/email/leave_application_email_subject.txt
+++ b/small_small_hr/templates/small_small_hr/email/leave_application_email_subject.txt
@@ -1 +1 @@
-{{subject}}
\ No newline at end of file
+{{ object.content_object.staff.get_name }} requested time off on {{ object.content_object.start|date:"d M" }} to {{ object.content_object.end|date:"d M" }}
diff --git a/small_small_hr/templates/small_small_hr/email/leave_completed_email_body.html b/small_small_hr/templates/small_small_hr/email/leave_completed_email_body.html
new file mode 100644
index 0000000..a2e32b4
--- /dev/null
+++ b/small_small_hr/templates/small_small_hr/email/leave_completed_email_body.html
@@ -0,0 +1,11 @@
+{{ object.content_object.staff.get_name }},
+Your time off request for {{ object.content_object.duration.days }} days of {{ object.content_object.get_leave_type_display }} from {{ object.content_object.start|date:"d M" }} - {{ object.content_object.end|date:"d M" }} has been {{ object.content_object.get_review_status_display|lower }}.
+{{ object.content_object.start|date:"D, d M Y" }} - {{ object.content_object.end|date:"D, d M Y" }}
+{{ object.content_object.get_leave_type_display}}
+{{ object.content_object.duration.days }} days
+Status: {{ object.content_object.get_review_status_display }}
+
+Thank you,
+{{SITE.name}}
+------
+http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/leave_completed_email_body.txt b/small_small_hr/templates/small_small_hr/email/leave_completed_email_body.txt
new file mode 100644
index 0000000..955f22f
--- /dev/null
+++ b/small_small_hr/templates/small_small_hr/email/leave_completed_email_body.txt
@@ -0,0 +1,15 @@
+{{ object.content_object.staff.get_name }},
+
+Your time off request for {{ object.content_object.duration.days }} days of {{ object.content_object.get_leave_type_display }} from {{ object.content_object.start|date:"d M" }} - {{ object.content_object.end|date:"d M" }} has been {{ object.content_object.get_review_status_display|lower }}.
+
+{{ object.content_object.start|date:"D, d M Y" }} - {{ object.content_object.end|date:"D, d M Y" }}
+{{ object.content_object.get_leave_type_display}}
+{{ object.content_object.duration.days }} days
+Status: {{ object.content_object.get_review_status_display }}
+
+Thank you,
+
+
+{{SITE.name}}
+------
+http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/leave_completed_email_subject.txt b/small_small_hr/templates/small_small_hr/email/leave_completed_email_subject.txt
new file mode 100644
index 0000000..0a0819f
--- /dev/null
+++ b/small_small_hr/templates/small_small_hr/email/leave_completed_email_subject.txt
@@ -0,0 +1 @@
+Your time off request of {{ object.content_object.start|date:"d M" }} - {{ object.content_object.end|date:"d M" }} has a response
diff --git a/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.html b/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.html
index f0d3095..6a9078b 100644
--- a/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.html
+++ b/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.html
@@ -1,5 +1,7 @@
-Hello,
-{{message|linebreaks}}
+{% load small_small_hr %}{{ object.content_object.staff.get_name }} requested overtime:
+{{ object.content_object.duration|overtime_duration }} on {{ object.content_object.date|date:"D, d M Y" }}
+{{ object.content_object.start|date:"P" }} - {{ object.content_object.end|date:"P" }}
+Please log in to process the above: http://{{SITE.name}}/reviews/{{ object.pk }}
Thank you,
{{SITE.name}}
diff --git a/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.txt b/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.txt
index a1e77af..6130123 100644
--- a/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.txt
+++ b/small_small_hr/templates/small_small_hr/email/overtime_application_email_body.txt
@@ -1,9 +1,13 @@
-Hello,
+{% load small_small_hr %}{{ object.content_object.staff.get_name }} requested overtime:
-{{message}}
+{{ object.content_object.duration|overtime_duration }} on {{ object.content_object.date|date:"D, d M Y" }}
+{{ object.content_object.start|date:"P" }} - {{ object.content_object.end|date:"P" }}
+
+Please log in to process the above: http://{{SITE.name}}/reviews/{{ object.pk }}
Thank you,
+
{{SITE.name}}
------
http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/overtime_application_email_subject.txt b/small_small_hr/templates/small_small_hr/email/overtime_application_email_subject.txt
index b975387..5865f0f 100644
--- a/small_small_hr/templates/small_small_hr/email/overtime_application_email_subject.txt
+++ b/small_small_hr/templates/small_small_hr/email/overtime_application_email_subject.txt
@@ -1 +1 @@
-{{subject}}
\ No newline at end of file
+{{ object.content_object.staff.get_name }} requested overtime on {{ object.content_object.date|date:"d M" }}
diff --git a/small_small_hr/templates/small_small_hr/email/overtime_completed_email_body.html b/small_small_hr/templates/small_small_hr/email/overtime_completed_email_body.html
new file mode 100644
index 0000000..571349c
--- /dev/null
+++ b/small_small_hr/templates/small_small_hr/email/overtime_completed_email_body.html
@@ -0,0 +1,9 @@
+{% load small_small_hr %}{{ object.content_object.staff.get_name }},
+Your overtime request for {{ object.content_object.duration|overtime_duration }} has been {{ object.content_object.get_review_status_display|lower }}.
+{{ object.content_object.duration|overtime_duration }} on {{ object.content_object.date|date:"D, d M Y" }}
+{{ object.content_object.start|date:"P" }} - {{ object.content_object.end|date:"P" }}
+Status: {{ object.content_object.get_review_status_display }}
+Thank you,
+{{SITE.name}}
+------
+http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/overtime_completed_email_body.txt b/small_small_hr/templates/small_small_hr/email/overtime_completed_email_body.txt
new file mode 100644
index 0000000..4b90eca
--- /dev/null
+++ b/small_small_hr/templates/small_small_hr/email/overtime_completed_email_body.txt
@@ -0,0 +1,13 @@
+{% load small_small_hr %}{{ object.content_object.staff.get_name }},
+
+Your overtime request for {{ object.content_object.duration|overtime_duration }} has been {{ object.content_object.get_review_status_display|lower }}.
+
+{{ object.content_object.duration|overtime_duration }} on {{ object.content_object.date|date:"D, d M Y" }}
+{{ object.content_object.start|date:"P" }} - {{ object.content_object.end|date:"P" }}
+Status: {{ object.content_object.get_review_status_display }}
+
+Thank you,
+
+{{SITE.name}}
+------
+http://{{SITE.domain}}
diff --git a/small_small_hr/templates/small_small_hr/email/overtime_completed_email_subject.txt b/small_small_hr/templates/small_small_hr/email/overtime_completed_email_subject.txt
new file mode 100644
index 0000000..3642088
--- /dev/null
+++ b/small_small_hr/templates/small_small_hr/email/overtime_completed_email_subject.txt
@@ -0,0 +1 @@
+Your overtime request of {{ object.content_object.date|date:"d M" }} has a response
diff --git a/small_small_hr/templatetags/__init__.py b/small_small_hr/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/small_small_hr/templatetags/small_small_hr.py b/small_small_hr/templatetags/small_small_hr.py
new file mode 100644
index 0000000..92a8657
--- /dev/null
+++ b/small_small_hr/templatetags/small_small_hr.py
@@ -0,0 +1,17 @@
+"""Template tags module."""
+from django import template
+from django.utils.translation import ugettext as _
+
+from small_small_hr.constants import HOURS, MINUTES
+
+register = template.Library()
+
+
+@register.filter
+def overtime_duration(time_delta):
+ """Display overtime duration."""
+ total_seconds = int(time_delta.total_seconds())
+ hours = total_seconds // 3600
+ minutes = (total_seconds % 3600) // 60
+
+ return f"{hours} {_(HOURS)} {minutes} {_(MINUTES)}"
diff --git a/small_small_hr/utils.py b/small_small_hr/utils.py
index bcaeb77..5304502 100644
--- a/small_small_hr/utils.py
+++ b/small_small_hr/utils.py
@@ -6,17 +6,16 @@
from django.conf import settings
from django.utils import timezone
-from small_small_hr.models import AnnualLeave, FreeDay, Leave
+from small_small_hr.models import AnnualLeave, FreeDay, Leave, StaffProfile
-def get_carry_over(staffprofile: object, year: int, leave_type: str):
- """
- Get carried over leave days
- """
+def get_carry_over(staffprofile: StaffProfile, year: int, leave_type: str):
+ """Get carried over leave days."""
# pylint: disable=no-member
if leave_type == Leave.REGULAR:
previous_obj = AnnualLeave.objects.filter(
- staff=staffprofile, year=year - 1, leave_type=leave_type).first()
+ staff=staffprofile, year=year - 1, leave_type=leave_type
+ ).first()
if previous_obj:
remaining = previous_obj.get_available_leave_days()
max_carry_over = settings.SSHR_MAX_CARRY_OVER
@@ -30,14 +29,13 @@ def get_carry_over(staffprofile: object, year: int, leave_type: str):
return 0
-def create_annual_leave(staffprofile: object, year: int, leave_type: str):
- """
- Creates an annuall leave object for the staff member
- """
+def create_annual_leave(staffprofile: StaffProfile, year: int, leave_type: str):
+ """Creates an annual leave object for the staff member."""
# pylint: disable=no-member
try:
annual_leave = AnnualLeave.objects.get(
- staff=staffprofile, year=year, leave_type=leave_type)
+ staff=staffprofile, year=year, leave_type=leave_type
+ )
except AnnualLeave.DoesNotExist:
carry_over = get_carry_over(staffprofile, year, leave_type)
@@ -51,17 +49,16 @@ def create_annual_leave(staffprofile: object, year: int, leave_type: str):
year=year,
leave_type=leave_type,
allowed_days=allowed_days,
- carried_over_days=carry_over
+ carried_over_days=carry_over,
)
annual_leave.save()
return annual_leave
-def create_free_days(
- start_year: int = timezone.now().year, number_of_years: int = 11):
+def create_free_days(start_year: int = timezone.now().year, number_of_years: int = 11):
"""
- Create FreeDay records
+ Create FreeDay records.
:param start_year: the year from which to start creating free days
:param number_of_years: number of years to create free days objects
@@ -71,12 +68,7 @@ def create_free_days(
for year in years:
for default_day in default_days:
the_date = date(
- year=year,
- month=default_day['month'],
- day=default_day['day'],
- )
- free_day = FreeDay(
- name=the_date.strftime("%A %d %B %Y"),
- date=the_date,
+ year=year, month=default_day["month"], day=default_day["day"],
)
+ free_day = FreeDay(name=the_date.strftime("%A %d %B %Y"), date=the_date,)
free_day.save()
diff --git a/tests/settings.py b/tests/settings.py
index f837e54..172c8c4 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -8,30 +8,31 @@
INSTALLED_APPS = [
# core django apps
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'django.contrib.sites',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "django.contrib.sites",
# third party
- 'sorl.thumbnail',
- 'private_storage',
- 'phonenumber_field',
- 'crispy_forms',
- 'rest_framework',
+ "sorl.thumbnail",
+ "private_storage",
+ "phonenumber_field",
+ "crispy_forms",
+ "rest_framework",
# custom
- 'small_small_hr',
+ "small_small_hr",
+ "model_reviews",
]
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql',
- 'NAME': 'small_small_hr',
- 'USER': 'postgres',
- 'PASSWORD': '',
- 'HOST': '127.0.0.1'
+ "default": {
+ "ENGINE": "django.db.backends.postgresql",
+ "NAME": "small_small_hr",
+ "USER": "postgres",
+ "PASSWORD": "",
+ "HOST": "127.0.0.1",
}
}
@@ -47,33 +48,36 @@
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [os.path.join(BASE_DIR, 'templates')],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [os.path.join(BASE_DIR, "templates")],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
],
},
},
]
-TIME_ZONE = 'Africa/Nairobi'
+TIME_ZONE = "Africa/Nairobi"
USE_I18N = True
USE_L10N = True
USE_TZ = True
SECRET_KEY = "i love oov"
-PRIVATE_STORAGE_ROOT = '/tmp/'
-MEDIA_ROOT = '/tmp/'
-PRIVATE_STORAGE_AUTH_FUNCTION = 'private_storage.permissions.allow_staff'
+PRIVATE_STORAGE_ROOT = "/tmp/"
+MEDIA_ROOT = "/tmp/"
+PRIVATE_STORAGE_AUTH_FUNCTION = "private_storage.permissions.allow_staff"
SITE_ID = 1
+# snapshot testing
+TEST_RUNNER = "snapshottest.django.TestRunner"
+
# try and load local_settings if present
try:
# pylint: disable=wildcard-import
diff --git a/tests/snapshots/__init__.py b/tests/snapshots/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/snapshots/snap_test_emails.py b/tests/snapshots/snap_test_emails.py
new file mode 100644
index 0000000..aa64913
--- /dev/null
+++ b/tests/snapshots/snap_test_emails.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# snapshottest: v1 - https://goo.gl/zC4yUc
+from __future__ import unicode_literals
+
+from snapshottest import Snapshot
+
+
+snapshots = Snapshot()
+
+snapshots['TestEmails::test_leave_emails 1'] = '''Mosh Pitt requested time off:
+
+5 days of Regular Leave
+Mon, 05 Jun 2017 - Sat, 10 Jun 2017
+Available Balance: 17.00 days
+
+Please log in to process the above: http://example.com/reviews/1338
+
+Thank you,
+
+
+example.com
+------
+http://example.com
+'''
+
+snapshots['TestEmails::test_leave_emails 2'] = 'Mosh Pitt requested time off:
5 days of Regular Leave
Mon, 05 Jun 2017 - Sat, 10 Jun 2017
Available Balance: 17.00 days
Please log in to process the above: http://example.com/reviews/1338
Thank you,
example.com
------
http://example.com'
+
+snapshots['TestEmails::test_overtime_emails 1'] = '''Mosh Pitt requested overtime:
+
+4 hours 45 minutes on Mon, 05 Jun 2017
+4:45 p.m. - 9:30 p.m.
+
+Please log in to process the above: http://example.com/reviews/1337
+
+Thank you,
+
+
+example.com
+------
+http://example.com
+'''
+
+snapshots['TestEmails::test_overtime_emails 2'] = 'Mosh Pitt requested overtime:
4 hours 45 minutes on Mon, 05 Jun 2017
4:45 p.m. - 9:30 p.m.
Please log in to process the above: http://example.com/reviews/1337
Thank you,
example.com
------
http://example.com'
+
+snapshots['TestEmails::test_leave_emails 3'] = '''Mosh Pitt,
+
+Your time off request for 5 days of Regular Leave from 05 Jun - 10 Jun has been approved.
+
+Mon, 05 Jun 2017 - Sat, 10 Jun 2017
+Regular Leave
+5 days
+Status: Approved
+
+Thank you,
+
+
+example.com
+------
+http://example.com
+'''
+
+snapshots['TestEmails::test_leave_emails 4'] = 'Mosh Pitt,
Your time off request for 5 days of Regular Leave from 05 Jun - 10 Jun has been approved.
Mon, 05 Jun 2017 - Sat, 10 Jun 2017
Regular Leave
5 days
Status: Approved
Thank you,
example.com
------
http://example.com'
+
+snapshots['TestEmails::test_leave_emails 5'] = '''Mosh Pitt,
+
+Your time off request for 5 days of Regular Leave from 05 Jun - 10 Jun has been rejected.
+
+Mon, 05 Jun 2017 - Sat, 10 Jun 2017
+Regular Leave
+5 days
+Status: Rejected
+
+Thank you,
+
+
+example.com
+------
+http://example.com
+'''
+
+snapshots['TestEmails::test_leave_emails 6'] = 'Mosh Pitt,
Your time off request for 5 days of Regular Leave from 05 Jun - 10 Jun has been rejected.
Mon, 05 Jun 2017 - Sat, 10 Jun 2017
Regular Leave
5 days
Status: Rejected
Thank you,
example.com
------
http://example.com'
+
+snapshots['TestEmails::test_overtime_emails 3'] = '''Mosh Pitt,
+
+Your overtime request for 4 hours 45 minutes has been approved.
+
+4 hours 45 minutes on Mon, 05 Jun 2017
+4:45 p.m. - 9:30 p.m.
+Status: Approved
+
+Thank you,
+
+example.com
+------
+http://example.com
+'''
+
+snapshots['TestEmails::test_overtime_emails 4'] = 'Mosh Pitt,
Your overtime request for 4 hours 45 minutes has been approved.
4 hours 45 minutes on Mon, 05 Jun 2017
4:45 p.m. - 9:30 p.m.
Status: ApprovedThank you,
example.com
------
http://example.com'
+
+snapshots['TestEmails::test_overtime_emails 5'] = '''Mosh Pitt,
+
+Your overtime request for 4 hours 45 minutes has been rejected.
+
+4 hours 45 minutes on Mon, 05 Jun 2017
+4:45 p.m. - 9:30 p.m.
+Status: Rejected
+
+Thank you,
+
+example.com
+------
+http://example.com
+'''
+
+snapshots['TestEmails::test_overtime_emails 6'] = 'Mosh Pitt,
Your overtime request for 4 hours 45 minutes has been rejected.
4 hours 45 minutes on Mon, 05 Jun 2017
4:45 p.m. - 9:30 p.m.
Status: RejectedThank you,
example.com
------
http://example.com'
diff --git a/tests/test_emails.py b/tests/test_emails.py
index 2ec2846..e5026e7 100644
--- a/tests/test_emails.py
+++ b/tests/test_emails.py
@@ -1,286 +1,189 @@
-"""
-Module to test small_small_hr Emails
-"""
+"""Module to test small_small_hr Emails."""
from datetime import datetime
-from unittest.mock import call, patch
from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
from django.core import mail
-from django.test import TestCase, override_settings
+from django.test import override_settings
import pytz
+from freezegun import freeze_time
from model_mommy import mommy
+from model_reviews.forms import PerformReview
+from model_reviews.models import ModelReview, Reviewer
+from snapshottest.django import TestCase
-from small_small_hr.emails import (leave_application_email,
- leave_processed_email,
- overtime_application_email,
- overtime_processed_email, send_email)
-from small_small_hr.models import Leave, OverTime
+from small_small_hr.forms import ApplyLeaveForm, ApplyOverTimeForm
+from small_small_hr.models import Leave, StaffProfile
+from small_small_hr.utils import create_annual_leave
-@override_settings(
- SSHR_ADMIN_EMAILS=["admin@example.com"],
- SSHR_ADMIN_LEAVE_EMAILS=["hr@example.com"],
- SSHR_ADMIN_OVERTIME_EMAILS=["ot@example.com"],
- SSHR_ADMIN_NAME="mosh"
-)
+@override_settings(ROOT_URLCONF="tests.urls")
class TestEmails(TestCase):
- """
- Test class for emails
- """
+ """Test class for emails."""
+
+ maxDiff = None
def setUp(self):
- """
- Set up
- """
+ """Set up."""
self.user = mommy.make(
- 'auth.User', first_name='Bob', last_name='Ndoe',
- email="bob@example.com")
- self.staffprofile = mommy.make(
- 'small_small_hr.StaffProfile', user=self.user)
-
- @patch('small_small_hr.emails.send_email')
- def test_leave_application_email(self, mock):
- """
- Test leave_application_email
- """
- start = datetime(
- 2017, 6, 5, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- end = datetime(
- 2017, 6, 10, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- leave = mommy.make(
- 'small_small_hr.Leave', staff=self.staffprofile, start=start,
- end=end, leave_type=Leave.SICK,
- status=Leave.PENDING)
-
- leave_application_email(leave)
-
- mock.assert_called_with(
- name="mosh",
- email="hr@example.com",
- subject="New Leave Application",
- message="There has been a new leave application. Please log in to process it.", # noqa
- obj=leave,
- template="leave_application",
+ "auth.User", first_name="Mosh", last_name="Pitt", email="bob@example.com"
)
+ self.staffprofile = mommy.make("small_small_hr.StaffProfile", user=self.user)
+ self.staffprofile.leave_days = 17
+ self.staffprofile.sick_days = 9
+ self.staffprofile.save()
+ self.staffprofile.refresh_from_db()
- @patch('small_small_hr.emails.send_email')
- def test_leave_processed_email(self, mock):
- """
- Test leave_processed_email
- """
- start = datetime(
- 2017, 6, 5, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- end = datetime(
- 2017, 6, 10, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- leave = mommy.make(
- 'small_small_hr.Leave', staff=self.staffprofile, start=start,
- end=end, leave_type=Leave.SICK,
- status=Leave.APPROVED)
+ create_annual_leave(self.staffprofile, 2017, Leave.REGULAR)
- leave_processed_email(leave)
+ StaffProfile.objects.rebuild()
- mock.assert_called_with(
- name="Bob Ndoe",
- email="bob@example.com",
- subject="Your leave application has been processed",
- message="You leave application status is Approved. Log in for more info.", # noqa
- obj=leave,
- cc_list=['hr@example.com']
+ hr_group = mommy.make("auth.Group", name=settings.SSHR_ADMIN_USER_GROUP_NAME)
+ self.boss = mommy.make(
+ "auth.User", first_name="Mother", last_name="Hen", email="hr@example.com"
)
-
- @patch('small_small_hr.emails.send_email')
- def test_overtime_application_email(self, mock):
- """
- Test overtime_application_email
- """
- start = datetime(
- 2017, 6, 5, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- end = datetime(
- 2017, 6, 10, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- overtime = mommy.make(
- 'small_small_hr.OverTime', staff=self.staffprofile, start=start,
- end=end, status=OverTime.PENDING)
-
- overtime_application_email(overtime)
-
- mock.assert_called_with(
- name="mosh",
- email="ot@example.com",
- subject="New Overtime Application",
- message="There has been a new overtime application. Please log in to process it.", # noqa
- obj=overtime,
- template="overtime_application",
+ self.boss.groups.add(hr_group)
+
+ @freeze_time("June 1st, 2017")
+ def test_leave_emails(self):
+ """Test Leave emails."""
+ # apply for leave
+ start = datetime(2017, 6, 5, 7, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
+ end = datetime(2017, 6, 10, 7, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
+ data = {
+ "staff": self.staffprofile.id,
+ "leave_type": Leave.REGULAR,
+ "start": start,
+ "end": end,
+ "review_reason": "Need a break",
+ }
+ form = ApplyLeaveForm(data=data)
+ self.assertTrue(form.is_valid())
+ leave = form.save()
+
+ obj_type = ContentType.objects.get_for_model(leave)
+ # Hard code the pk for the snapshot test
+ # empty the test outbox so that we don't deal with the old review's emails
+ mail.outbox = []
+ ModelReview.objects.get(content_type=obj_type, object_id=leave.id).delete()
+ review = mommy.make(
+ "model_reviews.ModelReview",
+ content_type=obj_type,
+ object_id=leave.id,
+ id=1338,
)
+ reviewer = Reviewer.objects.get(review=review, user=self.boss)
- @patch('small_small_hr.emails.send_email')
- def test_overtime_processed_email(self, mock):
- """
- Test overtime_processed_email
- """
- start = datetime(
- 2017, 6, 5, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- end = datetime(
- 2017, 6, 10, 0, 0, 0, tzinfo=pytz.timezone(settings.TIME_ZONE))
- overtime = mommy.make(
- 'small_small_hr.OverTime', staff=self.staffprofile, start=start,
- end=end, status=OverTime.REJECTED)
-
- overtime_processed_email(overtime)
-
- mock.assert_called_with(
- name="Bob Ndoe",
- email="bob@example.com",
- subject="Your overtime application has been processed",
- message="You overtime application status is Rejected. Log in for more info.", # noqa
- obj=overtime,
- cc_list=['ot@example.com']
+ self.assertEqual(
+ "Mosh Pitt requested time off on 05 Jun to 10 Jun", mail.outbox[0].subject
)
+ self.assertEqual(["Mother Hen
The quick brown fox.