diff --git a/base/admin.py b/base/admin.py index 308437356..f71922c51 100644 --- a/base/admin.py +++ b/base/admin.py @@ -7,6 +7,8 @@ from django.contrib import admin from simple_history.admin import SimpleHistoryAdmin from base.models import ( + Announcement, + Attachment, Company, Department, DynamicEmailConfiguration, @@ -53,4 +55,10 @@ admin.site.register(MultipleApprovalManagers) admin.site.register(ShiftrequestComment) admin.site.register(WorktyperequestComment) admin.site.register(DynamicPagination) +admin.site.register(Announcement) +admin.site.register(Attachment) + + + + diff --git a/base/announcement.py b/base/announcement.py new file mode 100644 index 000000000..c83914289 --- /dev/null +++ b/base/announcement.py @@ -0,0 +1,251 @@ +from django.http import HttpResponse, JsonResponse +from django.shortcuts import redirect, render +from base.forms import AnnouncementForm, AnnouncementcommentForm +from base.models import Announcement, AnnouncementComment +from employee.models import EmployeeWorkInformation +from horilla.decorators import login_required +from django.contrib import messages +from django.utils.translation import gettext_lazy as _ +from notifications.signals import notify +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger + + + +@login_required +def announcement_view(request): + + """ + This method is used to render all announcemnts. + """ + + announcement_list = Announcement.objects.all().order_by('-created_on') + + # Set the number of items per page + items_per_page = 10 + + paginator = Paginator(announcement_list, items_per_page) + + page = request.GET.get('page') + try: + announcements = paginator.page(page) + except PageNotAnInteger: + # If the page is not an integer, deliver the first page. + announcements = paginator.page(1) + except EmptyPage: + # If the page is out of range (e.g., 9999), deliver the last page of results. + announcements = paginator.page(paginator.num_pages) + + return render(request, "announcement/announcement.html", {'announcements': announcements}) + + + +@login_required +def create_announcement(request): + + """ + This method renders form and template to update Announcement + """ + + form = AnnouncementForm() + if request.method == "POST": + form = AnnouncementForm(request.POST, request.FILES) + if form.is_valid(): + anou,attachment_ids = form.save(commit=False) + anou.save() + anou.attachments.set(attachment_ids) + departments = form.cleaned_data["department"] + job_positions = form.cleaned_data["job_position"] + anou.department.set(departments) + anou.job_position.set(job_positions) + messages.success(request, _("Announcement created successfully.")) + + depar = [] + jobs = [] + emp_dep = [] + emp_jobs = [] + for i in departments: + depar.append(i.id) + for i in job_positions: + jobs.append(i.id) + + for i in depar: + emp = EmployeeWorkInformation.objects.filter(department_id = i) + for i in emp: + name = i.employee_id + emp_dep.append(name.employee_user_id) + + for i in jobs: + emp = EmployeeWorkInformation.objects.filter(job_position_id = i) + for i in emp: + name = i.employee_id + emp_jobs.append(name.employee_user_id) + + notify.send( + request.user.employee_get, + recipient=emp_dep, + verb="Your department was mentioned in a post.", + verb_ar="تم ذكر قسمك في منشور.", + verb_de="Ihr Abteilung wurde in einem Beitrag erwähnt.", + verb_es="Tu departamento fue mencionado en una publicación.", + verb_fr="Votre département a été mentionné dans un post.", + redirect="/announcement", + icon="chatbox-ellipses", + ) + + notify.send( + request.user.employee_get, + recipient=emp_jobs, + verb="Your job position was mentioned in a post.", + verb_ar="تم ذكر وظيفتك في منشور.", + verb_de="Ihre Arbeitsposition wurde in einem Beitrag erwähnt.", + verb_es="Tu puesto de trabajo fue mencionado en una publicación.", + verb_fr="Votre poste de travail a été mentionné dans un post.", + redirect="/announcement", + icon="chatbox-ellipses", + ) + + response = render( + request, "announcement/announcement_form.html", {"form": form} + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render(request, "announcement/announcement_form.html", {"form": form}) + + +@login_required +def delete_announcement(request, anoun_id): + + """ + This method is used to delete announcemnts. + """ + + announcement = Announcement.objects.filter(id=anoun_id) + announcement.delete() + messages.success(request, _("Announcement deleted successfully.")) + return redirect(announcement_view) + + +@login_required +def update_announcement(request, anoun_id): + + """ + This method renders form and template to update Announcement + """ + + announcement = Announcement.objects.get(id=anoun_id) + form = AnnouncementForm(instance = announcement) + + if request.method == "POST": + form = AnnouncementForm(request.POST, request.FILES, instance=announcement) + if form.is_valid(): + anou,attachment_ids = form.save(commit=False) + announcement = anou.save() + anou.attachments.set(attachment_ids) + departments = form.cleaned_data["department"] + job_positions = form.cleaned_data["job_position"] + anou.department.set(departments) + anou.job_position.set(job_positions) + messages.success(request, _("Announcement updated successfully.")) + + depar = [] + jobs = [] + emp_dep = [] + emp_jobs = [] + for i in departments: + depar.append(i.id) + for i in job_positions: + jobs.append(i.id) + + for i in depar: + emp = EmployeeWorkInformation.objects.filter(department_id = i) + for i in emp: + name = i.employee_id + emp_dep.append(name.employee_user_id) + + for i in jobs: + emp = EmployeeWorkInformation.objects.filter(job_position_id = i) + for i in emp: + name = i.employee_id + emp_jobs.append(name.employee_user_id) + + notify.send( + request.user.employee_get, + recipient=emp_dep, + verb="Your department was mentioned in a post.", + verb_ar="تم ذكر قسمك في منشور.", + verb_de="Ihr Abteilung wurde in einem Beitrag erwähnt.", + verb_es="Tu departamento fue mencionado en una publicación.", + verb_fr="Votre département a été mentionné dans un post.", + redirect="/announcement", + icon="chatbox-ellipses", + ) + + notify.send( + request.user.employee_get, + recipient=emp_jobs, + verb="Your job position was mentioned in a post.", + verb_ar="تم ذكر وظيفتك في منشور.", + verb_de="Ihre Arbeitsposition wurde in einem Beitrag erwähnt.", + verb_es="Tu puesto de trabajo fue mencionado en una publicación.", + verb_fr="Votre poste de travail a été mentionné dans un post.", + redirect="/announcement", + icon="chatbox-ellipses", + ) + + response = render( + request, "announcement/announcement_update_form.html", {"form": form} + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render(request, "announcement/announcement_update_form.html", {"form": form}) + + +@login_required +def create_announcement_comment(request, anoun_id): + """ + This method renders form and template to create Announcement comments + """ + anoun = Announcement.objects.filter(id=anoun_id).first() + emp = request.user.employee_get + form = AnnouncementcommentForm( + initial={"employee_id": emp.id, "request_id": anoun_id} + ) + + if request.method == "POST": + form = AnnouncementcommentForm(request.POST) + if form.is_valid(): + form.instance.employee_id = emp + form.instance.announcement_id = anoun + form.save() + form = AnnouncementcommentForm( + initial={"employee_id": emp.id, "request_id": anoun_id} + ) + messages.success(request, _("You commented a post.")) + return HttpResponse("") + return render( + request, + "announcement/comment_form.html", + {"form": form, "request_id": anoun_id}, + ) + + +@login_required +def comment_view(request, anoun_id): + """ + This method is used to view all comments in the announcements + """ + comments = AnnouncementComment.objects.filter(announcement_id=anoun_id).order_by( + "-created_at" + ) + no_comments = False + if not comments.exists(): + no_comments = True + + return render( + request, + "announcement/comment_view.html", + {"comments": comments, "no_comments": no_comments}, + ) + diff --git a/base/forms.py b/base/forms.py index 5ce61b8d3..b9404fe21 100644 --- a/base/forms.py +++ b/base/forms.py @@ -5,6 +5,7 @@ This module is used to register forms for base module """ import calendar import os +from typing import Any import uuid import datetime from datetime import timedelta @@ -19,6 +20,9 @@ from django.template.loader import render_to_string from employee.filters import EmployeeFilter from employee.models import Employee, EmployeeTag from base.models import ( + Announcement, + AnnouncementComment, + Attachment, Company, Department, DynamicEmailConfiguration, @@ -1617,4 +1621,78 @@ class DynamicPaginationForm(ModelForm): fields = "__all__" exclude = ('user_id',) - \ No newline at end of file + + +class MultipleFileInput(forms.ClearableFileInput): + allow_multiple_selected = True + + +class MultipleFileField(forms.FileField): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", MultipleFileInput()) + super().__init__(*args, **kwargs) + + def clean(self, data, initial=None): + single_file_clean = super().clean + if isinstance(data, (list, tuple)): + result = [single_file_clean(d, initial) for d in data] + else: + result = [single_file_clean(data, initial),] + return result[0] if result else None + + +class AnnouncementForm(ModelForm): + """ + Announcement Form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = Announcement + fields = '__all__' + excluded_fields = ['created_on'] + widgets = { + "description": forms.Textarea(attrs={"data-summernote": ""}), + } + + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["attachments"] = MultipleFileField(label="Attachments ") + self.fields["attachments"].required=False + + def save(self, commit: bool = ...) -> Any: + attachement = [] + multiple_attachment_ids = [] + attachements = None + if self.files.getlist("attachments"): + attachements = self.files.getlist("attachments") + self.instance.attachement = attachements[0] + multiple_attachment_ids = [] + + for attachement in attachements: + file_instance = Attachment() + file_instance.file = attachement + file_instance.save() + multiple_attachment_ids.append(file_instance.pk) + instance = super().save(commit) + if commit: + instance.attachements.add(*multiple_attachment_ids) + return instance, multiple_attachment_ids + +class AnnouncementcommentForm(ModelForm): + """ + Announcement comment form + """ + + class Meta: + """ + Meta class for additional options + """ + + model = AnnouncementComment + fields = ('comment',) + diff --git a/base/models.py b/base/models.py index 3a3e0db8f..cb2b67c5f 100644 --- a/base/models.py +++ b/base/models.py @@ -1159,4 +1159,44 @@ class DynamicPagination(models.Model): super().save(*args, **kwargs) def __str__(self): return (f"{self.user_id}|{self.pagination}") - \ No newline at end of file + + +class Attachment(models.Model): + """ + Attachment model for multiple attachments in announcemnts. + """ + file = models.FileField(upload_to='attachments/') + + def __str__(self): + return self.file.name + +class Announcement(models.Model): + + """ + Anonuncement Model for stroing all announcements. + """ + title = models.CharField(max_length=30) + description = models.TextField(null=True) + attachments = models.ManyToManyField(Attachment, related_name='announcement_attachments', blank=True) + created_on = models.DateTimeField(auto_now_add=True) + department = models.ManyToManyField(Department, blank=True) + job_position = models.ManyToManyField(JobPosition, blank=True) + + def __str__(self): + return self.title + + +class AnnouncementComment(models.Model): + """ + AnnouncementComment Model + """ + from employee.models import Employee + + announcement_id = models.ForeignKey(Announcement, on_delete=models.CASCADE) + employee_id = models.ForeignKey(Employee, on_delete=models.CASCADE) + comment = models.TextField(null=True, verbose_name=_("Comment")) + created_at = models.DateTimeField( + auto_now_add=True, + verbose_name=_("Created At"), + null=True, + ) diff --git a/base/templates/announcement/announcement.html b/base/templates/announcement/announcement.html new file mode 100644 index 000000000..ca43ab082 --- /dev/null +++ b/base/templates/announcement/announcement.html @@ -0,0 +1,512 @@ +{% extends "index.html" %} +{% load i18n %} +{% load static %} +{% load basefilters %} +{% block content %} + +{% comment %}
+
+ {% trans "Announcements" %} +
+
+ +
+ +
+ +
+
+
+
+ + + +{% if perms.announcemnt.add_announcemnt or request.user|is_reportingmanager %} +
+
+ + + +
{% trans "Create Announcements." %}
+
+
+{% endif %} + +
+ + {% for anoun in announcements %} +
+ + {% if perms.announcemnt.add_announcemnt or request.user|is_reportingmanager %} +
+ + +
+ {% endif %} + +
+
{{ anoun.title }}
+ + + {% trans "Posted on" %}  {{ anoun.created_on|date:"F j, Y" }}   + {% trans "at" %}   {{ anoun.created_on|time:"g:i A" }} + +
+ +
+

{{ anoun.description|safe }}

+
+ + {% if anoun.department.all %} + + {% endif %} + + {% if anoun.job_position.all %} + + {% endif %} + + + + + + + + +
+ {% endfor %} {% endcomment %} + + + + + + + + + + + + + + + + + + + +
+
+

{% trans "Announcements" %}

+ +
+ {% if perms.announcemnt.add_announcemnt or request.user|is_reportingmanager %} +
+ +
+ +
+ +
+
+
+ {% endif %} +
+ +
+ {% for anoun in announcements %} +
+
+
+ {{ anoun.title }}
+ + {% trans "Posted on" %}  {{ anoun.created_on|date:"F j, Y" }}   + {% trans "at" %}   {{ anoun.created_on|time:"g:i A" }} + + +
+ + {% if perms.announcemnt.add_announcemnt or request.user|is_reportingmanager %} +
+ +
+ +
+
+ {% endif %} +
+
+
+

{{ anoun.description|safe }}

+ + {% if anoun.department.all %} +
+ {% trans "Department" %} +
+ {% for dep in anoun.department.all %} + #{{ dep.department }} + {% endfor %} +
+
+ {% endif %} + + + {% if anoun.job_position.all %} +
+ {% trans "Job Position" %} +
+ {% for job in anoun.job_position.all %} + #{{ job.job_position }} + {% endfor %} +
+
+ {% endif %} + + {% for attachment in anoun.attachments.all %} + {% if anoun.attachments.all|length > 1 %} +
+ {% endif %} + {% if attachment.file.url|slice:"-4:" == '.png' or attachment.file.url|slice:"-4:" == '.jpg' or attachment.file.url|slice:"-5:" == '.jpeg' or attachment.file.url|slice:"-4:" == '.gif' or attachment.file.url|slice:"-4:" == '.bmp' or attachment.file.url|slice:"-5:" == '.webp' or attachment.file.url|slice:"-5:" == '.tiff' or attachment.file.url|slice:"-4:" == '.tif' or attachment.file.url|slice:"-4:" == '.svg' %} + +
+ + +
+ {% else %} + + {% endif %} + {% endfor %} + + +
+ + +
+
+ +
+ {% endfor %} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {% trans "Page" %} {{ announcements.number }} {% trans "of" %} {{ announcements.paginator.num_pages }}. + + +
+ + + +
+ + + + + + + +
+
+
+
{% trans "Edit Announcement." %} + +
+
+ +
+
+
+ + + + + + + + + + + +{% endblock content %} diff --git a/base/templates/announcement/announcement_form.html b/base/templates/announcement/announcement_form.html new file mode 100644 index 000000000..148bcc587 --- /dev/null +++ b/base/templates/announcement/announcement_form.html @@ -0,0 +1,27 @@ +{% load i18n %} +{% if form.errors %} + +
+
+ {% for error in form.non_field_errors %} +
+ {{ error }} +
+ {% endfor %} +
+
+{% endif %} +
+ {% csrf_token %} + {{form.as_p}} + +
\ No newline at end of file diff --git a/base/templates/announcement/announcement_update_form.html b/base/templates/announcement/announcement_update_form.html new file mode 100644 index 000000000..28e27d08d --- /dev/null +++ b/base/templates/announcement/announcement_update_form.html @@ -0,0 +1,27 @@ +{% load i18n %} +{% if form.errors %} + +
+
+ {% for error in form.non_field_errors %} +
+ {{ error }} +
+ {% endfor %} +
+
+{% endif %} +
+ {% csrf_token %} + {{form.as_p}} + +
\ No newline at end of file diff --git a/base/templates/announcement/comment_form.html b/base/templates/announcement/comment_form.html new file mode 100644 index 000000000..a7ad5c111 --- /dev/null +++ b/base/templates/announcement/comment_form.html @@ -0,0 +1,27 @@ +{% load i18n %} +{% if form.errors %} + +
+
+ {% for error in form.non_field_errors %} +
+ {{ error }} +
+ {% endfor %} +
+
+{% endif %} +
+ {% csrf_token %} + {{form.as_p}} + +
\ No newline at end of file diff --git a/base/templates/announcement/comment_view.html b/base/templates/announcement/comment_view.html new file mode 100644 index 000000000..d52597a4e --- /dev/null +++ b/base/templates/announcement/comment_view.html @@ -0,0 +1,63 @@ +{% load basefilters %} +{% load i18n %} + +{% if no_comments %} + +
+
+
+ {% trans "There is no comments to show." %} + +
+
+
+ +{% else %} + + {% for comment in comments %} +
+
+
+ {% trans "Comment" %} + {% comment %} + {% endcomment %} + +
+ {{comment.comment}} +
+
+
+
+
+
+ +
+ {% trans "Date & Time" %} + + {% trans "on" %}  {{ comment.created_at|date:"F j, Y" }}   + {% trans "at" %}   {{ comment.created_at|time:"g:i A" }} + +
+ +
+
+
+ + + {% endfor %} + +{% endif %} + + diff --git a/base/templates/base/action_type/action_type.html b/base/templates/base/action_type/action_type.html new file mode 100644 index 000000000..111665ad2 --- /dev/null +++ b/base/templates/base/action_type/action_type.html @@ -0,0 +1,70 @@ +{% extends 'settings.html' %} +{% load i18n %} +{% block settings %} +
+ +
+

{% trans "Action Type" %}

+ {% if perms.employee.add_actiontype %} + + {% endif %} +
+ {% include 'base/action_type/action_type_view.html' %} + +
+ + + + + + + + + + +{% endblock settings %} \ No newline at end of file diff --git a/base/templates/base/action_type/action_type_form.html b/base/templates/base/action_type/action_type_form.html new file mode 100644 index 000000000..ad3c75c57 --- /dev/null +++ b/base/templates/base/action_type/action_type_form.html @@ -0,0 +1,21 @@ +{% load i18n %} +
+ {% csrf_token %} + {{form.non_field_errors}} + {{form.as_p}} + + +
diff --git a/base/templates/base/action_type/action_type_view.html b/base/templates/base/action_type/action_type_view.html new file mode 100644 index 000000000..f93aec565 --- /dev/null +++ b/base/templates/base/action_type/action_type_view.html @@ -0,0 +1,57 @@ +{% load i18n %} +
+
+
+
+
{% trans "Actions" %}
+
{% trans "Type" %}
+ {% if perms.employee.change_actiontype or perms.employee.delete_actiontype %} +
{% trans "Options" %}
+ {% endif %} +
+
+
+ {% for act in action_types %} +
+
{{act.title}}
+
{{act.get_action_type_display}}
+ {% if perms.employee.change_actiontype or perms.employee.delete_actiontype %} +
+
+ {% if perms.employee.change_actiontype %} + + + {% endif %} + {% if perms.employee.delete_actiontype %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+ {% endif %} +
+ {% endfor %} +
+
+
diff --git a/base/urls.py b/base/urls.py index 514baf93b..f0c5bd259 100644 --- a/base/urls.py +++ b/base/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from base import request_and_approve, views +from base import request_and_approve, views, announcement from base.forms import RotatingShiftAssignForm, RotatingWorkTypeAssignForm, RotatingWorkTypeForm from base.models import ( Company, @@ -707,4 +707,49 @@ urlpatterns = [ name="dashboard-asset-request-approve", ), path('settings/pagination-settings-view/',views.pagination_settings_view,name="pagination-settings-view"), + + + path("settings/action-type/", views.action_type_view, name="action-type"), + path("action-type-create", views.action_type_create, name="action-type-create"), + path( + "action-type-update/", + views.action_type_update, + name="action-type-update", + ), + path( + "action-type-delete/", + views.action_type_delete, + name="action-type-delete", + ), + + + path('pagination-settings-view',views.pagination_settings_view,name="pagination-settings-view"), + + path( + "announcement/", + announcement.announcement_view, name="announcement" + ), + path( + "create-announcement", + announcement.create_announcement, name="create-announcement" + ), + path( + "delete-announcement/", + announcement.delete_announcement, name="delete-announcement" + ), + path( + "update-announcement/", + announcement.update_announcement, name="update-announcement" + ), + path( + "announcement-add-comment//", + announcement.create_announcement_comment, + name="announcement-add-comment", + ), + path( + "announcement-view-comment//", + announcement.comment_view, + name="announcement-view-comment", + ), + ] diff --git a/base/views.py b/base/views.py index 1779ed869..0aac51df3 100644 --- a/base/views.py +++ b/base/views.py @@ -22,6 +22,7 @@ from django.contrib.auth.models import Group, User, Permission from attendance.forms import AttendanceValidationConditionForm from attendance.models import AttendanceValidationCondition, GraceTime from django.views.decorators.csrf import csrf_exempt +from employee.forms import ActiontypeForm from horilla_audit.models import AuditTag from notifications.signals import notify from horilla.decorators import ( @@ -31,7 +32,7 @@ from horilla.decorators import ( manager_can_enter, ) from horilla.settings import EMAIL_HOST_USER -from employee.models import Employee, EmployeeTag, EmployeeWorkInformation +from employee.models import Actiontype, Employee, EmployeeTag, EmployeeWorkInformation from base.decorators import ( shift_request_change_permission, work_type_request_change_permission, @@ -4194,4 +4195,68 @@ def pagination_settings_view(request): if form.is_valid(): form.save() messages.success(request, _("Default pagination updated.")) - return render(request, "base/dynamic_pagination/pagination_settings.html", {"form": form}) \ No newline at end of file + return render(request, "base/dynamic_pagination/pagination_settings.html", {"form": form}) + + +@login_required +def action_type_view(request): + """ + This method is used to show Action Type + """ + action_types = Actiontype.objects.all() + return render( + request, "base/action_type/action_type.html", {"action_types": action_types} + ) + + +@login_required +def action_type_create(request): + """ + This method renders form and template to create Action Type + """ + form = ActiontypeForm() + if request.method == "POST": + form = ActiontypeForm(request.POST) + if form.is_valid(): + form.save() + form = ActiontypeForm() + messages.success(request, _("Action has been created successfully!")) + return HttpResponse("") + return render( + request, + "base/action_type/action_type_form.html", + { + "form": form, + }, + ) + + +@login_required +def action_type_update(request, act_id): + """ + This method renders form and template to update Action type + """ + action = Actiontype.objects.get(id=act_id) + form = ActiontypeForm(instance=action) + if request.method == "POST": + form = ActiontypeForm(request.POST, instance=action) + if form.is_valid(): + form.save() + form = ActiontypeForm() + messages.success(request, _("Action has been updated successfully!")) + return HttpResponse("") + return render( + request, + "base/action_type/action_type_form.html", + {"form": form, "act_id": act_id}, + ) + + +@login_required +def action_type_delete(request, act_id): + """ + This method is used to delete the action type. + """ + Actiontype.objects.filter(id=act_id).delete() + messages.success(request, _("Action has been deleted successfully!")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) diff --git a/static/images/ui/announcement.svg b/static/images/ui/announcement.svg new file mode 100644 index 000000000..b2dbcb601 --- /dev/null +++ b/static/images/ui/announcement.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html index 88b7a241c..9f06fa60a 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -204,6 +204,16 @@ > {% endif %} + +
  • + {% trans "Disciplinary Action Type" %} +
  • + {% if perms.base.view_company %}
  • diff --git a/templates/sidebar.html b/templates/sidebar.html index 089ba0066..e366f1d0f 100755 --- a/templates/sidebar.html +++ b/templates/sidebar.html @@ -200,7 +200,6 @@ > -
  • {% endif %} {% if perms.recruitment.view_skillzone %}
  • @@ -367,6 +366,14 @@ >
  • {% endif %} + +
  • + {% trans "Disciplinary Actions" %} +