diff --git a/.gitignore b/.gitignore index 497fd7a16..3801de163 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ node_modules/ FusionIIIT/static/ package-lock.json + +# migrations +migrations/ \ No newline at end of file diff --git a/FusionIIIT/applications/filetracking/api/serializers.py b/FusionIIIT/applications/filetracking/api/serializers.py new file mode 100644 index 000000000..bdae2024e --- /dev/null +++ b/FusionIIIT/applications/filetracking/api/serializers.py @@ -0,0 +1,24 @@ +from applications.filetracking.models import File, Tracking +from django.core.files import File as DjangoFile +from rest_framework import serializers + + +class FileSerializer(serializers.ModelSerializer): + class Meta: + model = File + fields = '__all__' + + +class TrackingSerializer(serializers.ModelSerializer): + class Meta: + model = Tracking + fields = '__all__' + + +class FileHeaderSerializer(serializers.ModelSerializer): + ''' + This serializes everything except the attachments of a file and whether it is read or not + ''' + class Meta: + model = File + exclude = ['upload_file', 'is_read'] diff --git a/FusionIIIT/applications/filetracking/api/urls.py b/FusionIIIT/applications/filetracking/api/urls.py new file mode 100644 index 000000000..139f965ff --- /dev/null +++ b/FusionIIIT/applications/filetracking/api/urls.py @@ -0,0 +1,24 @@ +from django.conf.urls import url +from .views import ( + CreateFileView, + ViewFileView, + ViewInboxView, + ViewOutboxView, + ViewHistoryView, + ForwardFileView, + DraftFileView, + CreateDraftFile, + GetDesignationsView, +) + +urlpatterns = [ + url(r'^file/$', CreateFileView.as_view(), name='create_file'), + url(r'^file/(?P\d+)/$', ViewFileView.as_view(), name='view_file'), + url(r'^inbox/$', ViewInboxView.as_view(), name='view_inbox'), + url(r'^outbox/$', ViewOutboxView.as_view(), name='view_outbox'), + url(r'^history/(?P\d+)/$', ViewHistoryView.as_view(), name='view_history'), + url(r'^forwardfile/(?P\d+)/$', ForwardFileView.as_view(), name='forward_file'), + url(r'^draft/$', DraftFileView.as_view(), name='view_drafts'), + url(r'^createdraft/$', CreateDraftFile.as_view(), name='create_draft'), + url(r'^designations/(?P\w+)/$', GetDesignationsView.as_view(), name='get_designations'), +] diff --git a/FusionIIIT/applications/filetracking/api/views.py b/FusionIIIT/applications/filetracking/api/views.py new file mode 100644 index 000000000..5f395ab41 --- /dev/null +++ b/FusionIIIT/applications/filetracking/api/views.py @@ -0,0 +1,247 @@ +import logging +from venv import logger +from django.forms import ValidationError +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status, permissions +from rest_framework.authentication import TokenAuthentication +from ..models import File, Tracking +from ..sdk.methods import create_draft, create_file, view_drafts, view_file, delete_file, view_inbox, view_outbox, view_history, forward_file, get_designations + +class CreateFileView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + try: + current_user = request.user.username + current_designation = request.data.get('designation') + receiver_username = request.data.get('receiver_username') + receiver_designation = request.data.get('receiver_designation') + subject = request.data.get('subject') + description = request.data.get('description') + + if None in [current_designation, receiver_username, receiver_designation, subject, description]: + return Response({'error': 'One or more required fields are missing.'}, status=status.HTTP_400_BAD_REQUEST) + + file_id = create_file(uploader=current_user, uploader_designation=current_designation, + receiver=receiver_username, receiver_designation=receiver_designation, subject=subject, description=description) + + return Response({'file_id': file_id}, status=status.HTTP_201_CREATED) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class ViewFileView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, file_id): + try: + file_details = view_file(int(file_id)) + # print(file_details) + return Response(file_details, status=status.HTTP_200_OK) + except ValueError: + return Response({'error': 'Invalid file ID format.'}, status=status.HTTP_400_BAD_REQUEST) + except File.DoesNotExist: + return Response({'error': 'File not found.'}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, file_id): + try: + # file_details = view_file(int(file_id)) + # print(file_details) + success = delete_file(int(file_id)) + if success: + return Response({'message': 'File deleted successfully'}, + status=status.HTTP_204_NO_CONTENT) + else : + return + + except ValueError: + return Response({'error': 'Invalid file ID format'}, + status=status.HTTP_400_BAD_REQUEST) + except File.DoesNotExist: + return Response({'error': 'File not found'}, + status=status.HTTP_404_NOT_FOUND) + except ValidationError as e: + return Response(e.detail, status=status.HTTP_400_BAD_REQUEST) # Handle ValidationError specifically + except Exception as e: # Catch unexpected errors + logger.error(f"Unexpected error in DeleteFileView: {e}") + return Response({'error': 'An internal server error occurred'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class ViewInboxView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def get(self, request): + """ + API endpoint to view inbox files. + + Expects query parameters: + - username (required): User requesting the inbox. + - designation (optional): Designation to filter files by. + - src_module (required): Source module to filter files by. + + Returns: + JSON response containing a list of serialized file data, including sender information. + """ + + username = request.query_params.get('username') + designation = request.query_params.get('designation') + src_module = request.query_params.get('src_module') + + + # if not username or not src_module: + # return Response({'error': 'Missing required query parameters: username and src_module.'}, status=400) + + inbox_files = view_inbox(username, designation, src_module) + return Response(inbox_files) + +class ViewOutboxView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + + def get(self, request): + """ + API endpoint to view outbox files. + + Expects query parameters: + - username (required): User requesting the outbox. + - designation (optional): Designation to filter files by. + - src_module (required): Source module to filter files by. + + Returns: + JSON response containing a paginated list of serialized file data. + """ + + username = request.query_params.get('username') + designation = request.query_params.get('designation') + src_module = request.query_params.get('src_module') + + + if not username or not src_module: + return Response({'error': 'Missing required query parameters: username and src_module.'}, status=400) + + outbox_files = view_outbox(username, designation, src_module) + return Response(outbox_files) + + +class ViewHistoryView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, file_id): + """ + View history of a particular file with the given file_id. + + Args: + request (rest_framework.request.Request): The incoming request object. + file_id (int): Primary key of the file to retrieve history for. + + Returns: + rest_framework.response.Response: JSON response containing serialized tracking history. + """ + + try: + history = view_history(file_id) + return Response(history) + except Tracking.DoesNotExist: + return Response({'error': f'File with ID {file_id} not found.'}, status=404) + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return Response({'error': 'Internal server error.'}, status=500) + +class ForwardFileView(APIView): +# # # Authentication and permission classes (adjust based on your needs) + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def post(self, request, file_id): +# # Extract data from request.data + receiver = request.data.get('receiver') + receiver_designation = request.data.get('receiver_designation') + file_extra_JSON = request.data.get('file_extra_JSON', {}) + remarks = request.data.get('remarks', "") + + # Validate data + if not receiver or not receiver_designation: + raise ValidationError("Missing required fields: receiver and receiver_designation") + + # # Extract and validate file attachment (if present) + file_attachment = request.FILES.get('file_attachment') + if file_attachment: + if file_attachment.size > 10 * 1024 * 1024: # Adjust size limit as needed + raise ValidationError("File size exceeds limit (10 MB)") + + # Call forward_file function + try: + new_tracking_id = forward_file( + int(file_id), + receiver, + receiver_designation, + file_extra_JSON, + remarks, + file_attachment + ) + logging.info(f"Successfully forwarded file {file_id} with tracking ID: {new_tracking_id}") + except Exception as e: + logging.error(f"Error forwarding file {file_id}: {str(e)}") + raise ValidationError(str(e)) # Re-raise exception with a user-friendly message + + # Return response + return Response({'tracking_id': new_tracking_id}, status=status.HTTP_201_CREATED) + +class CreateDraftFile(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def post(self, request): + uploader = request.data.get('uploader') + uploader_designation = request.data.get('uploader_designation') + src_module = request.data.get('src_module', 'filetracking') + src_object_id = request.data.get('src_object_id', '') + file_extra_JSON = request.data.get('file_extra_JSON', {}) + attached_file = request.FILES.get('attached_file', None) + + try: + file_id = create_draft( + uploader=uploader, + uploader_designation=uploader_designation, + src_module=src_module, + src_object_id=src_object_id, + file_extra_JSON=file_extra_JSON, + attached_file=attached_file + ) + return Response({'file_id': file_id}, status=status.HTTP_201_CREATED) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + +class DraftFileView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [permissions.IsAuthenticated] + + def get(self, request): + username = request.query_params.get('username') + designation = request.query_params.get('designation') + src_module = request.query_params.get('src_module') + + try: + draft_files = view_drafts(username, designation, src_module) + print(draft_files) + return Response(draft_files, status=status.HTTP_200_OK) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + +class GetDesignationsView(APIView): + #authentication_classes = [TokenAuthentication] + #permission_classes = [permissions.IsAuthenticated] + + def get(self, request, username, *args, **kwargs): + user_designations = get_designations(username) + return Response({'designations': user_designations}) diff --git a/FusionIIIT/applications/filetracking/decorators.py b/FusionIIIT/applications/filetracking/decorators.py new file mode 100644 index 000000000..05cc7cc51 --- /dev/null +++ b/FusionIIIT/applications/filetracking/decorators.py @@ -0,0 +1,38 @@ +from django.shortcuts import render, get_object_or_404 +from django.contrib.auth.models import User +from applications.globals.models import ExtraInfo, HoldsDesignation + + +def user_check(request): + """ + This function is used to check if the user is a student or not. + Its return type is bool. + @param: + request - contains metadata about the requested page + + @Variables: + current_user - get user from request + user_details - extract details of the user from the database + desig_id - check for designation + student - designation for a student + final_user - final designation of the request(our user) + """ + try: + current_user = get_object_or_404(User, username=request.user.username) + user_details = ExtraInfo.objects.select_related('user','department').get(user=request.user) + des = HoldsDesignation.objects.all().select_related().filter(user=request.user).first() + if str(des.designation) == "student": + return True + else: + return False + except Exception as e: + return False + +def user_is_student(view_func): + def _wrapped_view(request, *args, **kwargs): + if user_check(request): + return render(request, 'filetracking/fileTrackingNotAllowed.html') + else: + return view_func(request, *args, **kwargs) + return _wrapped_view + diff --git a/FusionIIIT/applications/filetracking/models.py b/FusionIIIT/applications/filetracking/models.py index 5f9581a08..9d78b24c9 100644 --- a/FusionIIIT/applications/filetracking/models.py +++ b/FusionIIIT/applications/filetracking/models.py @@ -16,6 +16,11 @@ class File(models.Model): is_read = models.BooleanField(default = False) + # additions for API + src_module = models.CharField(max_length=100, default='filetracking') + src_object_id = models.CharField(max_length=100,null=True) + file_extra_JSON = models.JSONField(null=True) + class Meta: db_table = 'File' @@ -25,13 +30,11 @@ class Meta: class Tracking(models.Model): """ - This is File Tracing Table which contains the status of each indivisual file created by the user + This is File Tracing Table which contains the status of each individual file created by the user """ file_id = models.ForeignKey(File, on_delete=models.CASCADE, null=True) current_id = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) current_design = models.ForeignKey(HoldsDesignation, null=True, on_delete=models.CASCADE) - # receiver_id = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='receiver_id') - # receive_design = models.ForeignKey(HoldsDesignation, null=True, on_delete=models.CASCADE, related_name='rec_design') receiver_id = models.ForeignKey(User,null = True, on_delete=models.CASCADE, related_name='receiver_id') receive_design = models.ForeignKey(Designation, null=True, on_delete=models.CASCADE, related_name='rec_design') @@ -41,5 +44,8 @@ class Tracking(models.Model): upload_file = models.FileField(blank=True) is_read = models.BooleanField(default = False) + # additions for API + tracking_extra_JSON = models.JSONField(null=True) + class Meta: db_table = 'Tracking' diff --git a/FusionIIIT/applications/filetracking/sdk/methods.py b/FusionIIIT/applications/filetracking/sdk/methods.py new file mode 100644 index 000000000..54e9b3574 --- /dev/null +++ b/FusionIIIT/applications/filetracking/sdk/methods.py @@ -0,0 +1,424 @@ +from amqp import NotFound +from django.contrib.auth.models import User +from applications.filetracking.models import Tracking, File +from applications.globals.models import Designation, HoldsDesignation, ExtraInfo +from applications.filetracking.api.serializers import FileSerializer, FileHeaderSerializer, TrackingSerializer +from django.core.exceptions import ValidationError +from typing import Any + + +def create_file( + uploader: str, + uploader_designation: str, + receiver: str, + receiver_designation: str, + subject: str = "", + description: str = "", + src_module: str = "filetracking", + src_object_id: str = "", + file_extra_JSON: dict = {}, + attached_file: Any = None) -> int: + ''' + This function is used to create a file object corresponding to any object of a module that needs to be tracked + ''' + + ''' + Functioning: + create base file with params + create tracking with params + if both complete then return id of file + else raise error + + also, delete file object if tracking isnt created + ''' + uploader_user_obj = get_user_object_from_username(uploader) + uploader_extrainfo_obj = get_ExtraInfo_object_from_username(uploader) + uploader_designation_obj = Designation.objects.get( + name=uploader_designation) + receiver_obj = get_user_object_from_username(receiver) + receiver_designation_obj = Designation.objects.get( + name=receiver_designation) + + new_file = File.objects.create( + uploader=uploader_extrainfo_obj, + subject=subject, + description=description, + designation=uploader_designation_obj, + src_module=src_module, + src_object_id=src_object_id, + file_extra_JSON=file_extra_JSON, + ) + + + if attached_file is not None: + new_file.upload_file.save(attached_file.name, attached_file, save=True) + + uploader_holdsdesignation_obj = HoldsDesignation.objects.get( + user=uploader_user_obj, designation=uploader_designation_obj) + + new_tracking = Tracking.objects.create( + file_id=new_file, + current_id=uploader_extrainfo_obj, + current_design=uploader_holdsdesignation_obj, + receiver_id=receiver_obj, + receive_design=receiver_designation_obj, + tracking_extra_JSON=file_extra_JSON, + remarks=f"File with id:{str(new_file.id)} created by {uploader} and sent to {receiver}" + # upload_file = None, dont add file for first tracking + ) + if new_tracking is None: + new_file.delete() + raise ValidationError('Tracking model data is incorrect') + else: + return new_file.id + + +def view_file(file_id: int) -> dict: + ''' + This function returns all the details of a given file + ''' + try: + requested_file = File.objects.get(id=file_id) + serializer = FileSerializer(requested_file) + file_details = serializer.data + return file_details + except File.DoesNotExist: + raise NotFound("File Not Found with provided ID") + + +def delete_file(file_id: int) -> bool: + ''' + This function is used to delete a file from being tracked, all the tracking history is deleted as well and returns true if the deletion was successful + ''' + try: + File.objects.filter(id=file_id).delete() + return True + except File.DoesNotExist: + return False + +# inbox and outbox could be sorted based on most recent linked tracking entry + +def view_inbox(username: str, designation: str, src_module: str) -> list: + ''' + This function is used to get all the files in the inbox of a particular user and designation + ''' + user_designation = Designation.objects.get(name=designation) + recipient_object = get_user_object_from_username(username) + received_files_tracking = Tracking.objects.select_related('file_id').filter( + receiver_id=recipient_object, + receive_design=user_designation, + file_id__src_module=src_module, + file_id__is_read=False).order_by('receive_date'); + received_files = [tracking.file_id for tracking in received_files_tracking] + + # remove duplicate file ids (from sending back and forth) + received_files_unique = uniqueList(received_files) + + received_files_serialized = list(FileHeaderSerializer( + received_files_unique, many=True).data) + + for file in received_files_serialized: + file['sent_by_user'] = get_last_file_sender(file['id']).username + file['sent_by_designation'] = get_last_file_sender_designation(file['id']).name + + return received_files_serialized + + +def view_outbox(username: str, designation: str, src_module: str) -> list: + ''' + This function is used to get all the files in the outbox of a particular user and designation + ''' + user_designation = get_designation_obj_from_name(designation=designation) + user_object = get_user_object_from_username(username) + user_HoldsDesignation_object = HoldsDesignation.objects.get( + user=user_object, designation=user_designation) + sender_ExtraInfo_object = get_ExtraInfo_object_from_username(username) + sent_files_tracking = Tracking.objects.select_related('file_id').filter( + current_id=sender_ExtraInfo_object, + current_design=user_HoldsDesignation_object, + file_id__src_module=src_module, + file_id__is_read=False).order_by('-receive_date') + sent_files = [tracking.file_id for tracking in sent_files_tracking] + + # remove duplicate file ids (from sending back and forth) + sent_files_unique = uniqueList(sent_files) + + sent_files_serialized = FileHeaderSerializer(sent_files_unique, many=True) + return sent_files_serialized.data + + + +def view_archived(username: str, designation: str, src_module: str) -> dict: + ''' + This function is used to get all the files in the archive of a particular user and designation + Archived file mean those which the user has ever interacted with, and are now finished or archived + ''' + user_designation = Designation.objects.get(name=designation) + user_object = get_user_object_from_username(username) + received_archived_tracking = Tracking.objects.select_related('file_id').filter( + receiver_id=user_object, + receive_design=user_designation, + file_id__src_module=src_module, + file_id__is_read=True) + + user_HoldsDesignation_object = HoldsDesignation.objects.get( + user=user_object, designation=user_designation) + sender_ExtraInfo_object = get_ExtraInfo_object_from_username(username) + sent_archived_tracking = Tracking.objects.select_related('file_id').filter( + current_id=sender_ExtraInfo_object, + current_design=user_HoldsDesignation_object, + file_id__src_module=src_module, + file_id__is_read=True) + + archived_tracking = received_archived_tracking | sent_archived_tracking + archived_files = [tracking.file_id for tracking in archived_tracking] + + # remove duplicate file ids (from sending back and forth) + archived_files_unique = uniqueList(archived_files) + + archived_files_serialized = FileHeaderSerializer(archived_files_unique, many=True) + return archived_files_serialized.data + + + +def archive_file(file_id: int) -> bool: + ''' + This function is used to archive a file and returns true if the archiving was successful + ''' + try: + File.objects.filter(id=file_id).update(is_read=True) + return True + except File.DoesNotExist: + return False + +def unarchive_file(file_id: int) -> bool: + ''' + This functions is used to unarchive a file and returns true if the unarchiving was successful + ''' + try: + File.objects.filter(id=file_id).update(is_read=False) + return True + except File.DoesNotExist: + return False + + + +def create_draft( + uploader: str, + uploader_designation: str, + src_module: str = "filetracking", + src_object_id: str = "", + file_extra_JSON: dict = {}, + attached_file: Any = None) -> int: + ''' + This function is used to create a draft file object corresponding to any object of a module that needs to be tracked + It is similar to create_file but is not sent to anyone + Later this file can be sent to someone by forward_file by using draft file_id + ''' + uploader_extrainfo_obj = get_ExtraInfo_object_from_username(uploader) + uploader_designation_obj = Designation.objects.get( + name=uploader_designation) + + new_file = File.objects.create( + uploader=uploader_extrainfo_obj, + designation=uploader_designation_obj, + src_module=src_module, + src_object_id=src_object_id, + file_extra_JSON=file_extra_JSON, + upload_file=attached_file + ) + return new_file.id + + +def view_drafts(username: str, designation: str, src_module: str) -> dict: + ''' + This function is used to get all the files in the drafts (has not been sent) of a particular user and designation + ''' + user_designation = Designation.objects.get(name=designation) + user_ExtraInfo_object = get_ExtraInfo_object_from_username(username) + draft_files = File.objects.filter( + tracking__isnull=True, uploader=user_ExtraInfo_object, designation=user_designation, src_module=src_module) + draft_files_serialized = FileHeaderSerializer(draft_files, many=True) + return draft_files_serialized.data + + + +def forward_file( + file_id: int, + receiver: str, + receiver_designation: str, + file_extra_JSON: dict, + remarks: str = "", + file_attachment: Any = None) -> int: + ''' + This function forwards the file and inserts a new tracking history into the file tracking table + Note that only the current owner(with appropriate designation) of the file has the ability to forward files + ''' + # HoldsDesignation and ExtraInfo object are used instead + # of Designation and User object because of the legacy code being that way + + current_owner = get_current_file_owner(file_id) + current_owner_designation = get_current_file_owner_designation(file_id) + current_owner_extra_info = ExtraInfo.objects.get(user=current_owner) + current_owner_holds_designation = HoldsDesignation.objects.get( + user=current_owner, designation=current_owner_designation) + receiver_obj = User.objects.get(username=receiver) + receiver_designation_obj = Designation.objects.get( + name=receiver_designation) + tracking_data = { + 'file_id': file_id, + 'current_id': current_owner_extra_info.id, + 'current_design': current_owner_holds_designation.id, + 'receiver_id': receiver_obj.id, + 'receive_design': receiver_designation_obj.id, + 'tracking_extra_JSON': file_extra_JSON, + 'remarks': remarks, + } + if file_attachment is not None: + tracking_data['upload_file'] = file_attachment + + tracking_entry = TrackingSerializer(data=tracking_data) + if tracking_entry.is_valid(): + tracking_entry.save() + return tracking_entry.instance.id + else: + raise ValidationError('forward data is incomplete') + + +def view_history(file_id: int) -> dict: + ''' + This function is used to get the history of a particular file with the given file_id + ''' + Tracking_history = Tracking.objects.filter( + file_id=file_id).order_by('-receive_date') + Tracking_history_serialized = TrackingSerializer( + Tracking_history, many=True) + return Tracking_history_serialized.data + + +# HELPER FUNCTIONS + +def get_current_file_owner(file_id: int) -> User: + ''' + This functions returns the current owner of the file. + The current owner is the latest recipient of the file + ''' + latest_tracking = Tracking.objects.filter( + file_id=file_id).order_by('-receive_date').first() + latest_recipient = latest_tracking.receiver_id + return latest_recipient + + +def get_current_file_owner_designation(file_id: int) -> Designation: + ''' + This function returns the designation of the current owner of the file. + The current owner is the latest recipient of the file + ''' + latest_tracking = Tracking.objects.filter( + file_id=file_id).order_by('-receive_date').first() + latest_recipient_designation = latest_tracking.receive_design + return latest_recipient_designation + +def get_last_file_sender(file_id: int) -> User: + ''' + This Function returns the last file sender, + one who has last forwarded/sent the file + ''' + latest_tracking = Tracking.objects.filter( + file_id=file_id).order_by('-receive_date').first() + latest_sender_extra_info = latest_tracking.current_id + return latest_sender_extra_info.user + +def get_last_file_sender_designation(file_id: int) -> Designation: + ''' + This Function returns the last file sender's Designation, + one who has last forwarded/sent the file + ''' + latest_tracking = Tracking.objects.filter( + file_id=file_id).order_by('receive_date').first() + latest_sender_holds_designation = latest_tracking.current_design + return latest_sender_holds_designation.designation + +def get_designations(username: str) -> list: + ''' + This function is used to return a list of all the designation names of a particular user + ''' + user = User.objects.get(username=username) + designations_held = HoldsDesignation.objects.filter(user=user) + designation_name = [hold_designation.designation.name for hold_designation in designations_held] + return designation_name + + +def get_user_object_from_username(username: str) -> User: + user = User.objects.get(username=username) + return user + +def get_ExtraInfo_object_from_username(username: str) -> ExtraInfo: + user = User.objects.get(username=username) + extrainfo = ExtraInfo.objects.get(user=user) + return extrainfo + +def uniqueList(l: list) -> list: + ''' + This function is used to return a list with unique elements + O(n) time and space + ''' + seen = set() + unique_list = [] + for item in l: + if item not in seen: + unique_list.append(item) + seen.add(item) + return unique_list + +def add_uploader_department_to_files_list(files: list) -> list: + ''' + This function is used to add the department of the uploader to the file + ''' + for file in files: + uploader_Extrainfo = file['uploader'] + file['uploader_department'] = (str(uploader_Extrainfo.department)).split(': ')[1] + + return files + +def get_designation_obj_from_name(designation: str) -> Designation: + des = Designation.objects.get(name = designation) + return des + +def get_HoldsDesignation_obj(username: str, designation:str) -> HoldsDesignation: + user_object = get_user_object_from_username(username=username) + user_designation = get_designation_obj_from_name(designation=designation) + obj = HoldsDesignation.objects.get( + user=user_object, designation=user_designation) + return obj + +def get_last_recv_tracking_for_user(file_id: int, username: str, designation: str)-> Tracking: + ''' + This returns the last tracking where username+designation recieved file_id + ''' + + recv_user_obj = get_user_object_from_username(username) + recv_design_obj = get_designation_obj_from_name(designation) + + last_tracking = Tracking.objects.filter(file_id=file_id, + receiver_id=recv_user_obj, + receive_design=recv_design_obj).order_by('-receive_date')[0] + return last_tracking + +def get_last_forw_tracking_for_user(file_id: int, username: str, designation: str) -> Tracking: + ''' + Returns the last tracking where the specified user forwarded the file. + ''' + + # Get user and designation objects + sender_user_obj = get_ExtraInfo_object_from_username(username) + sender_designation_obj = get_HoldsDesignation_obj(username=username, designation=designation) + + # Filter Tracking objects by file_id, sender_id, and sender_designation + last_tracking = Tracking.objects.filter(file_id=file_id, + current_id=sender_user_obj, + current_design=sender_designation_obj).order_by('-forward_date').first() + return last_tracking + +def get_extra_info_object_from_id(id: int): + return ExtraInfo.objects.get(id=id) diff --git a/FusionIIIT/applications/filetracking/urls.py b/FusionIIIT/applications/filetracking/urls.py index cb4a7563d..6a3c24cf9 100644 --- a/FusionIIIT/applications/filetracking/urls.py +++ b/FusionIIIT/applications/filetracking/urls.py @@ -1,31 +1,46 @@ -from django.conf.urls import url +from django.conf.urls import url, include from . import views +from .api import urls app_name = 'filetracking' urlpatterns = [ url(r'^$', views.filetracking, name='filetracking'), - url(r'^drafts/$', views.drafts, name='drafts'), - url(r'^fileview/(?P\d+)$', views.fileview, name='fileview'), - url(r'^fileview1/(?P\d+)$', views.fileview1, name='fileview1'), - url(r'^fileview2/(?P\d+)$', views.fileview2, name='fileview2'), + url(r'^draftdesign/$', views.draft_design, name='draft_design'), + url(r'^drafts/(?P\d+)$', views.drafts_view, name='drafts_view'), + url(r'^outbox/(?P\d+)$', views.outbox_view, name='outbox_view'), + url(r'^inbox/(?P\d+)$', views.inbox_view, name='inbox_view'), url(r'^outward/$', views.outward, name='outward'), url(r'^inward/$', views.inward, name='inward'), - url(r'^confirmdelete/(?P\d+)$', views.confirmdelete, name='confirm_delete'), - url(r'^archive/(?P\d+)/$', views.archive, name='archive'), - url(r'^finish/(?P\d+)/$', views.finish, name='finish'), + url(r'^confirmdelete/(?P\d+)$', + views.confirmdelete, name='confirm_delete'), + url(r'^archive/(?P\d+)/$', views.archive_view, name='archive_view'), + url(r'^finish/(?P\d+)/$', views.archive_file, name='finish_file'), + url(r'^viewfile/(?P\d+)/$', views.view_file, name='view_file_view'), url(r'^forward/(?P\d+)/$', views.forward, name='forward'), url(r'^ajax/$', views.AjaxDropdown1, name='ajax_dropdown1'), url(r'^ajax_dropdown/$', views.AjaxDropdown, name='ajax_dropdown'), - url(r'^test/$',views.test, name='test'), - url(r'^delete/(?P\d+)$',views.delete, name='delete'), - url(r'^forward_inward/(?P\d+)/$', views.forward_inward, name='forward_inward'), + url(r'^test/$', views.test, name='test'), + url(r'^delete/(?P\d+)$', views.delete, name='delete'), + url(r'^forward_inward/(?P\d+)/$', + views.forward_inward, name='forward_inward'), - ## correction team 24 + # correction team 24 url(r'^finish_design/$', views.finish_design, name='finish_design'), - url(r'^finish_fileview/(?P\d+)$', views.finish_fileview, name='finish_fileview'), + url(r'^finish_fileview/(?P\d+)$', + views.finish_fileview, + name='finish_fileview'), url(r'^archive_design/$', views.archive_design, name='archive_design'), - url(r'^archive_finish/(?P\d+)/$', views.archive_finish, name='archive_finish'), + url(r'^archive_finish/(?P\d+)/$', + views.archive_finish, name='archive_finish'), + url(r'unarchive/(?P\d+)/$', + views.unarchive_file, name='unarchive'), + url(r'^getdesignations/(?P\w+)/$', views.get_designations_view, name="get_user_designations"), + url(r'^editdraft/(?P\w+)/$', views.edit_draft_view, name="edit_draft"), + + # REST api urls + url(r'^api/', include(urls)) + ] diff --git a/FusionIIIT/applications/filetracking/views.py b/FusionIIIT/applications/filetracking/views.py index a3c0a94ee..397045b39 100644 --- a/FusionIIIT/applications/filetracking/views.py +++ b/FusionIIIT/applications/filetracking/views.py @@ -8,62 +8,17 @@ from django.db import IntegrityError from django.core import serializers from django.contrib.auth.models import User +from django.http import JsonResponse from timeit import default_timer as time -from notification.views import office_module_notif,file_tracking_notif +from notification.views import office_module_notif, file_tracking_notif from .utils import * +from django.utils.dateparse import parse_datetime +from .sdk.methods import * +from .decorators import * - -@login_required(login_url = "/accounts/login/") -### - -def errorCheck(request): - return render(request, 'filetracking/contactUs.html') - -#### - -def user_check(request): - """ - This function is used to check if the user is a student or not - Its return type is bool - @param: - request - contains metadata about the requested page - - @Variables: - current_user - get user from request - user_details - extract details of the user from the database - desig_id - check for designation - student - designation for a student - final_user - final designation of the request(our user) - - - """ - try: - current_user = get_object_or_404(User, username=request.user.username) - - #extra info details , user id used as main id - user_details = ExtraInfo.objects.select_related('user','department').get(user = request.user) - - des = HoldsDesignation.objects.all().select_related().filter(user = request.user).first() - print(str(des.designation)) - if str(des.designation) == "student": - return True - else: - return False - - except Exception as e: - return False - - - ################################### - - +@login_required(login_url="/accounts/login/") +@user_is_student def filetracking(request): - - if user_check(request): - return render(request, 'filetracking/fileTrackingNotAllowed.html') - - - """ The function is used to create files by current user(employee). It adds the employee(uploader) and file datails to a file(table) of filetracking(model) @@ -84,18 +39,21 @@ def filetracking(request): holdsdesignations - The HoldsDesignation object. context - Holds data needed to make necessary changes in the template. """ - if request.method =="POST": + print(request.POST) + if request.method == "POST": try: if 'save' in request.POST: uploader = request.user.extrainfo subject = request.POST.get('title') description = request.POST.get('desc') design = request.POST.get('design') - designation = Designation.objects.get(id = HoldsDesignation.objects.select_related('user','working','designation').get(id = design).designation_id) + designation = Designation.objects.get(id=HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(id=design).designation_id) upload_file = request.FILES.get('myfile') - if(upload_file.size / 1000 > 10240): - messages.error(request,"File should not be greater than 10MB") - return redirect("/filetracking") + if upload_file and upload_file.size / 1000 > 10240: + messages.error( + request, "File should not be greater than 10MB") + return redirect("/filetracking") File.objects.create( uploader=uploader, @@ -105,18 +63,20 @@ def filetracking(request): upload_file=upload_file ) - messages.success(request,'File Draft Saved Successfully') + messages.success(request, 'File Draft Saved Successfully') if 'send' in request.POST: uploader = request.user.extrainfo subject = request.POST.get('title') description = request.POST.get('desc') design = request.POST.get('design') - designation = Designation.objects.get(id = HoldsDesignation.objects.select_related('user','working','designation').get(id = design).designation_id) + designation = Designation.objects.get(id=HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(id=design).designation_id) upload_file = request.FILES.get('myfile') - if(upload_file.size / 1000 > 10240): - messages.error(request,"File should not be greater than 10MB") + if upload_file and upload_file.size / 1000 > 10240: + messages.error( + request, "File should not be greater than 10MB") return redirect("/filetracking") file = File.objects.create( @@ -127,12 +87,12 @@ def filetracking(request): upload_file=upload_file ) - current_id = request.user.extrainfo remarks = request.POST.get('remarks') sender = request.POST.get('design') - current_design = HoldsDesignation.objects.select_related('user','working','designation').get(id=sender) + current_design = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(id=sender) receiver = request.POST.get('receiver') try: @@ -140,8 +100,9 @@ def filetracking(request): except Exception as e: messages.error(request, 'Enter a valid Username') return redirect('/filetracking/') - receive = request.POST.get('recieve') + receive = request.POST.get('receive') try: + print(receive) receive_design = Designation.objects.get(name=receive) except Exception as e: messages.error(request, 'Enter a valid Designation') @@ -158,19 +119,19 @@ def filetracking(request): remarks=remarks, upload_file=upload_file, ) - #office_module_notif(request.user, receiver_id) - file_tracking_notif(request.user,receiver_id,subject) - messages.success(request,'File sent successfully') + # office_module_notif(request.user, receiver_id) + file_tracking_notif(request.user, receiver_id, subject) + messages.success(request, 'File sent successfully') except IntegrityError: message = "FileID Already Taken.!!" return HttpResponse(message) - - - file = File.objects.select_related('uploader__user','uploader__department','designation').all() - extrainfo = ExtraInfo.objects.select_related('user','department').all() - holdsdesignations = HoldsDesignation.objects.select_related('user','working','designation').all() + file = File.objects.select_related( + 'uploader__user', 'uploader__department', 'designation').all() + extrainfo = ExtraInfo.objects.select_related('user', 'department').all() + holdsdesignations = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').all() designations = get_designation(request.user) context = { @@ -182,11 +143,8 @@ def filetracking(request): return render(request, 'filetracking/composefile.html', context) -@login_required(login_url = "/accounts/login") - -def drafts(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## +@login_required(login_url="/accounts/login") +def draft_design(request): """ The function is used to get the designation of the user and renders it on draft template. @@ -194,26 +152,21 @@ def drafts(request): request - trivial. @variables: - - + + context - Holds data needed to make necessary changes in the template. """ - - ### designation = get_designation(request.user) context = { 'designation': designation, } - return render(request, 'filetracking/drafts.html', context) + return render(request, 'filetracking/draft_design.html', context) -@login_required(login_url = "/accounts/login") -def fileview(request,id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - +@login_required(login_url="/accounts/login") +def drafts_view(request, id): """ - This function is used to veiw all all created files by the user ordered by upload date.it collects all the created files from File object. + This function is used to view all the drafts created by the user ordered by upload date.it collects all the created files from File object. @param: request - trivial @@ -222,47 +175,36 @@ def fileview(request,id): @parameters draft - file obeject containing all the files created by user context - holds data needed to render the template - - - """ - # draft = File.objects.select_related('uploader__user','uploader__department','designation').filter(uploader=request.user.extrainfo).order_by('-upload_date') - # extrainfo = ExtraInfo.objects.select_related('user','department').all() - - extrainfo = ExtraInfo.objects.select_related('user','department').all() - - ids = File.objects.filter(uploader=request.user.extrainfo).order_by('-upload_date').values_list('id', flat=True) - draft_files_pk=[] - - for i in ids: - file_tracking_ids = Tracking.objects.filter(file_id=i).values_list('id', flat=True) - if(len(file_tracking_ids)==0): - draft_files_pk.append(i) - - draft_file_list=[] - for i in draft_files_pk: - draft_file_list.append(File.objects.get(pk=i)) + """ + user_HoldsDesignation_obj = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(pk=id) + s = str(user_HoldsDesignation_obj).split(" - ") + designation = s[1] + draft_files = view_drafts( + username=user_HoldsDesignation_obj.user, + designation=user_HoldsDesignation_obj.designation, + src_module='filetracking' + ) + + # Correct upload_date type + for f in draft_files: + f['upload_date'] = parse_datetime(f['upload_date']) + f['uploader'] = get_extra_info_object_from_id(f['uploader']) + + draft_files = add_uploader_department_to_files_list(draft_files) - user_designation = HoldsDesignation.objects.select_related('user','working','designation').get(pk=id) - s = str(user_designation).split(" - ") - designations = s[1] context = { - - 'draft': draft_file_list, - 'extrainfo': extrainfo, - 'designations': designations, + 'draft_files': draft_files, + 'designations': designation, } - return render(request, 'filetracking/fileview.html', context) - - + return render(request, 'filetracking/drafts.html', context) -@login_required(login_url = "/accounts/login") -def fileview1(request,id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## +@login_required(login_url="/accounts/login") +def outbox_view(request, id): """ The function is used to get all the files sent by user(employee) to other employees which are filtered from Tracking(table) objects by current user i.e. current_id. @@ -274,61 +216,89 @@ def fileview1(request,id): id - user id @variables: - out - The Tracking object filtered by current_id i.e, present working user. + outward_files - File objects filtered by current_id i.e, present working user. context - Holds data needed to make necessary changes in the template. - + """ + user_HoldsDesignation_obj = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(pk=id) + s = str(user_HoldsDesignation_obj).split(" - ") + designation = s[1] + + outward_files = view_outbox(username=user_HoldsDesignation_obj.user, + designation=user_HoldsDesignation_obj.designation, + src_module='filetracking') + + for f in outward_files: + last_forw_tracking = get_last_forw_tracking_for_user(file_id=f['id'], + username=user_HoldsDesignation_obj.user, + designation=user_HoldsDesignation_obj.designation) + f['sent_to_user'] = last_forw_tracking.receiver_id + f['sent_to_design'] = last_forw_tracking.receive_design + f['last_sent_date'] = last_forw_tracking.forward_date - outward_files = Tracking.objects.select_related('file_id__uploader__user','file_id__uploader__department','file_id__designation','current_id__user','current_id__department', - 'current_design__user','current_design__working','current_design__designation','receiver_id','receive_design').filter(current_id=request.user.extrainfo).order_by('-forward_date') + f['upload_date'] = parse_datetime(f['upload_date']) + f['uploader'] = get_extra_info_object_from_id(f['uploader']) - user_designation = HoldsDesignation.objects.select_related('user','working','designation').get(pk=id) + outward_files = add_uploader_department_to_files_list(outward_files) context = { - 'out': outward_files, - 'abcd': user_designation, + 'out_files': outward_files, + 'viewer_designation': designation, } - return render(request, 'filetracking/fileview1.html', context) + return render(request, 'filetracking/outbox.html', context) -@login_required(login_url = "/accounts/login") -def fileview2(request,id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - +@login_required(login_url="/accounts/login") +def inbox_view(request, id): """ The function is used to fetch the files received by the user form other employees. These files are filtered by receiver id and ordered by receive date. @param: request - trivial. - id - user id + id - HoldsDesignation object id @variables: - inward_file - The Tracking object filtered by receiver_id i.e, present working user. + inward_files - File object with additional sent by information context - Holds data needed to make necessary changes in the template. """ - inward_file = Tracking.objects.select_related('file_id__uploader__user','file_id__uploader__department','file_id__designation','current_id__user','current_id__department', - 'current_design__user','current_design__working','current_design__designation','receiver_id','receive_design').filter(receiver_id=request.user).order_by('-receive_date') - user_designation = HoldsDesignation.objects.select_related('user','working','designation').get(pk=id) - s = str(user_designation).split(" - ") - designations = s[1] + user_HoldsDesignation_obj = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(pk=id) + s = str(user_HoldsDesignation_obj).split(" - ") + designation = s[1] + inward_files = view_inbox( + username=user_HoldsDesignation_obj.user, + designation=user_HoldsDesignation_obj.designation, + src_module='filetracking' + ) + + # correct upload_date type and add recieve_date + for f in inward_files: + f['upload_date'] = parse_datetime(f['upload_date']) + + last_recv_tracking = get_last_recv_tracking_for_user(file_id=f['id'], + username=user_HoldsDesignation_obj.user, + designation=user_HoldsDesignation_obj.designation) + f['receive_date'] = last_recv_tracking.receive_date + f['uploader'] = get_extra_info_object_from_id(f['uploader']) + + inward_files = add_uploader_department_to_files_list(inward_files) + context = { - 'in_file': inward_file, - 'designations': designations, + 'in_file': inward_files, + 'designations': designation, } - return render(request, 'filetracking/fileview2.html', context) + return render(request, 'filetracking/inbox.html', context) -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def outward(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## """ This function fetches the different designations of the user and renders it on outward template @param: @@ -336,24 +306,22 @@ def outward(request): @variables: context - Holds the different designation data of the user - + """ designation = get_designation(request.user) context = { 'designation': designation, } - return render( request, 'filetracking/outward.html', context) + return render(request, 'filetracking/outward.html', context) -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def inward(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## """ This function fetches the different designations of the user and renders it on inward template - + @param: request - trivial. @@ -362,7 +330,7 @@ def inward(request): """ designation = get_designation(request.user) context = { - + 'designation': designation, } return render(request, 'filetracking/inward.html', context) @@ -370,9 +338,6 @@ def inward(request): @login_required(login_url = "/accounts/login") def confirmdelete(request,id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - """ The function is used to confirm the deletion of a file. @param: @@ -382,19 +347,87 @@ def confirmdelete(request,id): @variables: context - Holds data needed to make necessary changes in the template. """ - file = File.objects.select_related('uploader__user','uploader__department','designation').get(pk = id) + file = File.objects.select_related( + 'uploader__user', 'uploader__department', 'designation').get(pk=id) context = { 'j': file, } - return render(request, 'filetracking/confirmdelete.html',context) + return render(request, 'filetracking/confirmdelete.html', context) +@login_required(login_url="/accounts/login") +def view_file(request, id): + ''' + This function is used to view a particular file received by an employee from another. + This function also conditionally renders two forms 'forward_file' and 'archive_file' + based on if the user has necessary permissions or not. + The business permissions are as follows: + 1. User can forward file only if they are the last recipient of the file + 2. User can archive a file only if they have received it last and they are also the original owner of the file -@login_required(login_url = "/accounts/login") + To forward the file and to archive the file separate views with POST request are called + + It displays the details file of a File and remarks as well as the attachments of all the users + who have been involved till that point of the workflow. + + @param: + request - Trivial. + id - ID of the file object which the user intends to forward to another employee. + + @variables: + file - The File object. + track - The Tracking object. + designation - the designations of the user + ''' + + file = get_object_or_404(File, id=id) + track = Tracking.objects.select_related('file_id__uploader__user', 'file_id__uploader__department', 'file_id__designation', 'current_id__user', 'current_id__department', + 'current_design__user', 'current_design__working', 'current_design__designation', 'receiver_id', 'receive_design').filter(file_id=file).order_by('receive_date') + designations = get_designation(request.user) + + forward_enable = False + archive_enable = False + + current_owner = get_current_file_owner(file.id) + file_uploader = get_user_object_from_username(file.uploader.user.username) + + + if current_owner == request.user and file.is_read is False: + forward_enable = True + if current_owner == request.user and file_uploader == request.user and file.is_read is False: + archive_enable = True + + context = { + 'designations': designations, + 'file': file, + 'track': track, + 'forward_enable': forward_enable, + 'archive_enable': archive_enable, + } + return render(request, 'filetracking/viewfile.html', context) + +@login_required(login_url="/accounts/login") +def archive_file(request, id): + '''This function is used to archive a file. + It returns unauthorized access if the user is not file uploader + and the current owner of the file + ''' + if request.method == "POST": + file = get_object_or_404(File, id=id); + current_owner = get_current_file_owner(file.id) + file_uploader = get_user_object_from_username(file.uploader.user.username) + if current_owner == request.user and file_uploader == request.user: + file.is_read = True + file.save() + messages.success(request, 'File Archived') + else: + messages.error(request, 'Unauthorized access') + + return render(request, 'filetracking/composefile.html') + +@login_required(login_url="/accounts/login") def forward(request, id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## """ The function is used to forward files received by user(employee) from other employees which are filtered from Tracking(table) objects by current user @@ -419,73 +452,72 @@ def forward(request, id): holdsdesignations = HoldsDesignation objects. context - Holds data needed to make necessary changes in the template. """ - - file = get_object_or_404(File, id=id) - track = Tracking.objects.select_related('file_id__uploader__user','file_id__uploader__department','file_id__designation','current_id__user','current_id__department', - 'current_design__user','current_design__working','current_design__designation','receiver_id','receive_design').filter(file_id=file) - - if request.method == "POST": - if 'finish' in request.POST: - file.complete_flag = True - file.save() - - if 'send' in request.POST: - current_id = request.user.extrainfo - remarks = request.POST.get('remarks') - track.update(is_read=True) - sender = request.POST.get('sender') - current_design = HoldsDesignation.objects.select_related('user','working','designation').get(id=sender) - - receiver = request.POST.get('receiver') - try: - receiver_id = User.objects.get(username=receiver) - except Exception as e: - messages.error(request, 'Enter a valid destination') - designations = HoldsDesignation.objects.select_related('user','working','designation').filter(user=request.user) - - context = { - - 'designations': designations, - 'file': file, - 'track': track, - } - return render(request, 'filetracking/forward.html', context) - receive = request.POST.get('recieve') - try: - receive_design = Designation.objects.get(name=receive) - except Exception as e: - messages.error(request, 'Enter a valid Designation') - designations = get_designation(request.user) + file = get_object_or_404(File, id=id) + track = Tracking.objects.select_related('file_id__uploader__user', 'file_id__uploader__department', 'file_id__designation', 'current_id__user', 'current_id__department', + 'current_design__user', 'current_design__working', 'current_design__designation', 'receiver_id', 'receive_design').filter(file_id=file).order_by('receive_date') - context = { - - 'designations': designations, - 'file': file, - 'track': track, - } - return render(request, 'filetracking/forward.html', context) + if request.method == "POST": + if 'finish' in request.POST: + file.is_read = True + file.save() + if 'send' in request.POST: + current_id = request.user.extrainfo + remarks = request.POST.get('remarks') + track.update(is_read=True) + + sender = request.POST.get('sender') + current_design = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(id=sender) + + receiver = request.POST.get('receiver') + try: + receiver_id = User.objects.get(username=receiver) + except Exception as e: + messages.error(request, 'Enter a valid destination') + designations = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').filter(user=request.user) + + context = { + + 'designations': designations, + 'file': file, + 'track': track, + } + return render(request, 'filetracking/forward.html', context) + receive = request.POST.get('receive') + try: + receive_design = Designation.objects.get(name=receive) + except Exception as e: + messages.error(request, 'Enter a valid Designation') + designations = get_designation(request.user) + + context = { + + 'designations': designations, + 'file': file, + 'track': track, + } + return render(request, 'filetracking/forward.html', context) + + upload_file = request.FILES.get('myfile') + + Tracking.objects.create( + file_id=file, + current_id=current_id, + current_design=current_design, + receive_design=receive_design, + receiver_id=receiver_id, + remarks=remarks, + upload_file=upload_file, + ) + messages.success(request, 'File sent successfully') - - upload_file = request.FILES.get('myfile') - - Tracking.objects.create( - file_id=file, - current_id=current_id, - current_design=current_design, - receive_design=receive_design, - receiver_id=receiver_id, - remarks=remarks, - upload_file=upload_file, - ) - messages.success(request, 'File sent successfully') - - designations = get_designation(request.user) context = { - - 'designations':designations, + + 'designations': designations, 'file': file, 'track': track, } @@ -493,93 +525,88 @@ def forward(request, id): return render(request, 'filetracking/forward.html', context) -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def archive_design(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - - designation = HoldsDesignation.objects.select_related('user','working','designation').filter(user=request.user) + designation = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').filter(user=request.user) context = { 'designation': designation, } - return render( request, 'filetracking/archive_design.html', context) - - + return render(request, 'filetracking/archive_design.html', context) +@login_required(login_url="/accounts/login") +def archive_view(request, id): + """ + The function is used to fetch the files in the user's archive + (those which have passed by user and been archived/finished) -@login_required(login_url = "/accounts/login") -def archive(request , id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - - draft = File.objects.select_related('uploader__user','uploader__department','designation').filter(is_read=True).order_by('-upload_date') - + @param: + request - trivial. + id - HoldsDesignation object id - extrainfo = ExtraInfo.objects.select_related('user','department').all() - # designations = Designation.objects.filter(upload_designation=extrainfo.id) - abcd = HoldsDesignation.objects.select_related('user','working','designation').get(pk=id) - s = str(abcd).split(" - ") - designations = s[1] - #designations = HoldsDesignation.objects.filter(user=request.user) - # for x in designations: - # if abcd==x: - # designations=abcd + @variables: + archive_files - File object with additional information + context - Holds data needed to make necessary changes in the template. - context = { + """ + user_HoldsDesignation_obj = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(pk=id) + s = str(user_HoldsDesignation_obj).split(" - ") + designation = s[1] - 'draft': draft, - 'extrainfo': extrainfo, - 'designations': designations, - } + archive_files = view_archived( + username=user_HoldsDesignation_obj.user, + designation=user_HoldsDesignation_obj.designation, + src_module='filetracking' + ) + # correct upload_date type and add receive_date + for f in archive_files: + f['upload_date'] = parse_datetime(f['upload_date']) + f['designation'] = Designation.objects.get(id=f['designation']) + f['uploader'] = get_extra_info_object_from_id(f['uploader']) + archive_files = add_uploader_department_to_files_list(archive_files) + context = { + 'archive_files': archive_files, + 'designations': designation, + } + return render(request, 'filetracking/archive.html', context) - return render(request, 'filetracking/archive.html' , context) -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def archive_finish(request, id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - file1 = get_object_or_404(File, id=id) ##file = get_object_or_404(File, ref_id=id) + # file = get_object_or_404(File, ref_id=id) + file1 = get_object_or_404(File, id=id) track = Tracking.objects.filter(file_id=file1) - - return render(request, 'filetracking/archive_finish.html', {'file': file1, 'track': track}) - - -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def finish_design(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - designation = HoldsDesignation.objects.select_related('user','working','designation').filter(user=request.user) + designation = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').filter(user=request.user) context = { 'designation': designation, } - return render( request, 'filetracking/finish_design.html', context) + return render(request, 'filetracking/finish_design.html', context) -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def finish_fileview(request, id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - - out = Tracking.objects.select_related('file_id__uploader__user','file_id__uploader__department','file_id__designation','current_id__user','current_id__department', - 'current_design__user','current_design__working','current_design__designation','receiver_id','receive_design').filter(file_id__uploader=request.user.extrainfo, is_read=False).order_by('-forward_date') - + out = Tracking.objects.select_related('file_id__uploader__user', 'file_id__uploader__department', 'file_id__designation', 'current_id__user', 'current_id__department', + 'current_design__user', 'current_design__working', 'current_design__designation', 'receiver_id', 'receive_design').filter(file_id__uploader=request.user.extrainfo, is_read=False).order_by('-forward_date') - - abcd = HoldsDesignation.objects.select_related('user','working','designation').get(pk=id) - + abcd = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(pk=id) context = { @@ -589,42 +616,29 @@ def finish_fileview(request, id): return render(request, 'filetracking/finish_fileview.html', context) - - -@login_required(login_url = "/accounts/login") +@login_required(login_url="/accounts/login") def finish(request, id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - file1 = get_object_or_404(File, id=id) ##file = get_object_or_404(File, ref_id=id) + # file = get_object_or_404(File, ref_id=id) + file1 = get_object_or_404(File, id=id) track = Tracking.objects.filter(file_id=file1) - if request.method == "POST": - if 'Finished' in request.POST: - File.objects.filter(pk=id).update(is_read=True) - track.update(is_read=True) - messages.success(request,'File Archived') - - - - - - - - return render(request, 'filetracking/finish.html', {'file': file1, 'track': track,'fileid':id}) - + if 'Finished' in request.POST: + File.objects.filter(pk=id).update(is_read=True) + track.update(is_read=True) + messages.success(request, 'File Archived') + return render(request, 'filetracking/finish.html', {'file': file1, 'track': track, 'fileid': id}) def AjaxDropdown1(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## + """ This function returns the designation of receiver on the forward or compose file template. @param: request - trivial. - + @variables: context - return the httpresponce containing the matched designation of the user @@ -635,22 +649,19 @@ def AjaxDropdown1(request): hold = Designation.objects.filter(name__startswith=value) holds = serializers.serialize('json', list(hold)) context = { - 'holds' : holds + 'holds': holds } return HttpResponse(JsonResponse(context), content_type='application/json') def AjaxDropdown(request): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - """ This function returns the usernames of receiver on the forward or compose file template. @param: request - trivial. - + @variables: context - return the httpresponce containing the matched username @@ -673,28 +684,20 @@ def test(request): @login_required(login_url = "/accounts/login") def delete(request,id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - """ The function is used the delete of a file and it returns to the drafts page. @param: request - trivial. id - id of the file that is going to be deleted - + """ - file = File.objects.get(pk = id) + file = File.objects.get(pk=id) file.delete() - return redirect('/filetracking/drafts/') - - + return redirect('/filetracking/draftdesign/') def forward_inward(request,id): - if user_check(request):## - return render(request, 'filetracking/fileTrackingNotAllowed.html')## - """ This function is used forward the files which are available in the inbox of the user . @param: @@ -705,21 +708,135 @@ def forward_inward(request,id): file - file object track - tracking object of the file context - necessary data to render - + """ file = get_object_or_404(File, id=id) file.is_read = True - track = Tracking.objects.select_related('file_id__uploader__user','file_id__uploader__department','file_id__designation','current_id__user','current_id__department', - 'current_design__user','current_design__working','current_design__designation','receiver_id','receive_design').filter(file_id=file) + track = Tracking.objects.select_related('file_id__uploader__user', 'file_id__uploader__department', 'file_id__designation', 'current_id__user', 'current_id__department', + 'current_design__user', 'current_design__working', 'current_design__designation', 'receiver_id', 'receive_design').filter(file_id=file) designations = get_designation(request.user) context = { - - 'designations':designations, + + 'designations': designations, 'file': file, 'track': track, } return render(request, 'filetracking/forward.html', context) +def get_designations_view(request, username): + designations = get_designations(username) + return JsonResponse(designations, safe=False) +def unarchive_file(request, id): + try: + file = get_object_or_404(File, id=id) + file.is_read = False + file.save() + messages.success(request, 'File unarchived') + except File.DoesNotExist: + messages.error(request, 'File does not exist') + except Exception as e: + messages.error(request, 'Unable to unarchive: {}'.format(str(e))) + + return render(request, 'filetracking/archive.html') + + +@login_required(login_url="/accounts/login") +def edit_draft_view(request, id, *args, **kwargs): + """ + The function is used to edit and send drafted files, and also alter their title and subject + along with their remarks and attachments + + @param: + request - trivial. + id - id of the file object which the user intends to forward to other employee. + + @variables: + file - The File object. + track - The Tracking object. + remarks = Remarks posted by user. + receiver = Receiver to be selected by user for forwarding file. + receiver_id = Receiver_id who has been selected for forwarding file. + upload_file = File attached by user. + extrainfo = ExtraInfo object. + holdsdesignations = HoldsDesignation objects. + context - Holds data needed to make necessary changes in the template. + """ + + file = get_object_or_404(File, id=id) + track = Tracking.objects.select_related('file_id__uploader__user', 'file_id__uploader__department', 'file_id__designation', 'current_id__user', 'current_id__department', + 'current_design__user', 'current_design__working', 'current_design__designation', 'receiver_id', 'receive_design').filter(file_id=file).order_by('receive_date') + + if request.method == "POST": + if 'send' in request.POST: + current_id = request.user.extrainfo + remarks = request.POST.get('remarks') + subject = request.POST.get('subject') + description = request.POST.get('description') + + file.subject = subject + file.description = description + file.save() + track.update(is_read=True) + + sender = request.POST.get('sender') + current_design = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').get(id=sender) + + receiver = request.POST.get('receiver') + try: + receiver_id = User.objects.get(username=receiver) + except Exception as e: + messages.error(request, 'Enter a valid destination') + designations = HoldsDesignation.objects.select_related( + 'user', 'working', 'designation').filter(user=request.user) + + context = { + + 'designations': designations, + 'file': file, + 'track': track, + } + return render(request, 'filetracking/editdraft.html', context) + receive = request.POST.get('receive') + try: + receive_design = Designation.objects.get(name=receive) + except Exception as e: + messages.error(request, 'Enter a valid Designation') + designations = get_designation(request.user) + + context = { + + 'designations': designations, + 'file': file, + } + return render(request, 'filetracking/editdraft.html', context) + + upload_file = request.FILES.get('myfile') + + Tracking.objects.create( + file_id=file, + current_id=current_id, + current_design=current_design, + receive_design=receive_design, + receiver_id=receiver_id, + remarks=remarks, + upload_file=upload_file, + ) + messages.success(request, 'File sent successfully') + + designations = get_designation(request.user) + + context = { + + 'designations': designations, + 'file': file, + 'track': track, + } + + return render(request, 'filetracking/editdraft.html', context) + + + diff --git a/FusionIIIT/templates/filetracking/archive.html b/FusionIIIT/templates/filetracking/archive.html index ddf3fc617..1d58182e9 100644 --- a/FusionIIIT/templates/filetracking/archive.html +++ b/FusionIIIT/templates/filetracking/archive.html @@ -4,50 +4,39 @@ {% block filetracking_tab %}
- +
- + - - - - + + - {% for j in draft %} - {% ifequal designations j.designation|stringformat:'s' %} - - - - - - - - - - - {% endifequal %} - {% endfor %} + {% for f in archive_files %} + + + + + {% comment %} {% endcomment %} + + + {% endfor %} -
Created By File IDCreated By SubjectDateView FileForwardView Archived File
{{j.uploader.user}} - {{j.designation}}{{request.user.extrainfo.department.name}}-{{j.upload_date.year}}-{{j.upload_date.month}}-#{{j.id}}{{j.subject}}{{j.upload_date}}
{{f.uploader_department}}-{{f.upload_date.year}}-{{f.upload_date.month}}-#{{f.id}}{{f.uploader}}-{{f.designation}}{{f.subject|default:"None"}}{{f.upload_date}} + +
-
- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/FusionIIIT/templates/filetracking/archive_design.html b/FusionIIIT/templates/filetracking/archive_design.html index bf95ad805..c94542022 100644 --- a/FusionIIIT/templates/filetracking/archive_design.html +++ b/FusionIIIT/templates/filetracking/archive_design.html @@ -20,7 +20,7 @@

{% for designation in designation %} - {% endfor %} diff --git a/FusionIIIT/templates/filetracking/archive_finish.html b/FusionIIIT/templates/filetracking/archive_finish.html index 610882c04..25183fd48 100644 --- a/FusionIIIT/templates/filetracking/archive_finish.html +++ b/FusionIIIT/templates/filetracking/archive_finish.html @@ -2,51 +2,39 @@ {% load static %} {% block filetracking_tab %} - {% comment %}the compounder prescription tab starts here {% endcomment %} - {% comment %}the ambulance schedule tab starts here {% endcomment %}
- -
+ {% csrf_token %} -
@@ -130,32 +113,16 @@

- -
- +


- - - +
+ +

-
- -
-
- {% comment %}the compounder prescription tab ends here {% endcomment %} -{% endblock %} - - - \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/FusionIIIT/templates/filetracking/composefile.html b/FusionIIIT/templates/filetracking/composefile.html index 819e71479..f42e55f92 100644 --- a/FusionIIIT/templates/filetracking/composefile.html +++ b/FusionIIIT/templates/filetracking/composefile.html @@ -104,13 +104,9 @@

@@ -210,6 +206,37 @@

}); + + {% endblock %} + + + +{% endblock %} + diff --git a/FusionIIIT/templates/filetracking/fileTrackingNotAllowed.html b/FusionIIIT/templates/filetracking/fileTrackingNotAllowed.html index f2a8c0685..791e57f77 100644 --- a/FusionIIIT/templates/filetracking/fileTrackingNotAllowed.html +++ b/FusionIIIT/templates/filetracking/fileTrackingNotAllowed.html @@ -1,2 +1,41 @@ -

File Tracking Module is not allowed for Students. -

+{% extends 'globals/base.html' %} +{% load static %} + +{% block title %} + File Tracking +{% endblock %} + +{% block body %} + {% block navBar %} + {% include 'dashboard/navbar.html' %} + {% endblock %} + +
+
+ +
+
+
+ File Tracking Not Available +
+

This feature is not open for students.

+
+ Go to Dashboard +
+ +
+
+ +
+
+ +
+
+{% endblock %} + +{% block javascript %} + + + +{% endblock %} + diff --git a/FusionIIIT/templates/filetracking/filetracking.html b/FusionIIIT/templates/filetracking/filetracking.html index 4ae903241..17c81ed3f 100644 --- a/FusionIIIT/templates/filetracking/filetracking.html +++ b/FusionIIIT/templates/filetracking/filetracking.html @@ -13,7 +13,7 @@ {% endblock %} {% comment %}The grid starts here!{% endcomment %} -
+
{% comment %}The left-margin segment!{% endcomment %}
@@ -40,7 +40,7 @@ - + Drafts @@ -60,10 +60,10 @@ correction team 24 --> - + Archive @@ -96,7 +96,7 @@ {% comment %} The central-rail segment starts here! {% endcomment %} -
+
{% comment %}The ... start here!{% endcomment %}
{% block filetracking_tab %} @@ -106,7 +106,7 @@
{% comment %}The central-rail segment ends here!{% endcomment %} - {% comment %}The right-rail segment starts here!{% endcomment %} + {% comment %}The right-margin segment!{% endcomment %}
diff --git a/FusionIIIT/templates/filetracking/fileview.html b/FusionIIIT/templates/filetracking/fileview.html deleted file mode 100644 index 13e857cb3..000000000 --- a/FusionIIIT/templates/filetracking/fileview.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends 'filetracking/filetracking.html' %} -{% load static %} - -{% block filetracking_tab %} -
- -
- - - - - - - - - - - - - - {% for j in draft %} - {% ifequal designations j.designation|stringformat:'s' %} - - - - - - - - - - {% endifequal %} - {% endfor %} - - -
Created ByFile IDSubjectDateView FileDelete File
{{j.uploader.user}} - {{j.designation}}{{request.user.extrainfo.department.name}}-{{j.upload_date.year}}-{{j.upload_date.month}}-#{{j.id}}{{j.subject}}{{j.upload_date}}
- -
- -{% endblock %} \ No newline at end of file diff --git a/FusionIIIT/templates/filetracking/fileview1.html b/FusionIIIT/templates/filetracking/fileview1.html deleted file mode 100644 index 349111328..000000000 --- a/FusionIIIT/templates/filetracking/fileview1.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends 'filetracking/filetracking.html' %} -{% load static %} - -{% block filetracking_tab %} - - -
- - - - - - - - - - - - - - - {% for i in out %} - {% ifequal abcd i.current_design %} - - - - - - - - - - {% endifequal %} - {% endfor %} - - -
Sent AsSent ToFile IDSubjectDateView File
{{i.current_design}}{{i.receiver_id}}-{{i.receive_design}}{{i.file_id.uploader.department.name}}-{{i.file_id.upload_date.year}}-{{i.file_id.upload_date.month}}-#{{i.file_id.id}}{{i.file_id.subject}}{{i.receive_date}} -
-
- -{% endblock %} diff --git a/FusionIIIT/templates/filetracking/fileview2.html b/FusionIIIT/templates/filetracking/fileview2.html deleted file mode 100644 index b56ca2dcc..000000000 --- a/FusionIIIT/templates/filetracking/fileview2.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends 'filetracking/filetracking.html' %} -{% load static %} - -{% block filetracking_tab %} - - -
- - - - - - - - - - - - - - - - {% for j in in_file %} - {% ifequal designations j.receive_design|stringformat:'s' %} - - - - - - - - - - - - - - - - {% endifequal %} - {% endfor %} - - -
Received asSend byFile IDSubjectDateView File
{{request.user.extrainfo.user}} - {{j.receive_design}}{{j.current_design}}{{j.file_id.uploader.department.name}}-{{j.file_id.upload_date.year}}-{{j.file_id.upload_date.month}}-#{{j.file_id.id}} {{j.file_id.subject}}{{j.receive_date}}
- -
- -{% endblock %} diff --git a/FusionIIIT/templates/filetracking/forward.html b/FusionIIIT/templates/filetracking/forward.html index b585c7506..09106054a 100644 --- a/FusionIIIT/templates/filetracking/forward.html +++ b/FusionIIIT/templates/filetracking/forward.html @@ -41,8 +41,8 @@
{% for t in track %} -
-
+
+
@@ -359,6 +355,38 @@ }); + + + {% endblock %} diff --git a/FusionIIIT/templates/filetracking/inbox.html b/FusionIIIT/templates/filetracking/inbox.html new file mode 100644 index 000000000..a5619e4ca --- /dev/null +++ b/FusionIIIT/templates/filetracking/inbox.html @@ -0,0 +1,50 @@ +{% extends 'filetracking/filetracking.html' %} +{% load static %} +{% block filetracking_tab %} + +
+ + + + + + + + + + + + + + + {% for f in in_file %} + + + + + + + + {% endfor %} + + +
Sent byFile IDSubjectLast ReceivedView File
{{f.sent_by_user}}-{{f.sent_by_designation}}{{f.uploader_department}}-{{f.upload_date.year}}-{{f.upload_date.month}}-#{{f.id}} {{f.subject|default:"None"}}{{f.receive_date}} + +
+ +
+ +{% endblock %} diff --git a/FusionIIIT/templates/filetracking/inward.html b/FusionIIIT/templates/filetracking/inward.html index a42491ad2..5932089d8 100644 --- a/FusionIIIT/templates/filetracking/inward.html +++ b/FusionIIIT/templates/filetracking/inward.html @@ -20,7 +20,7 @@

{% for designation in designation %} - {% endfor %} diff --git a/FusionIIIT/templates/filetracking/outbox.html b/FusionIIIT/templates/filetracking/outbox.html new file mode 100644 index 000000000..416606d46 --- /dev/null +++ b/FusionIIIT/templates/filetracking/outbox.html @@ -0,0 +1,45 @@ +{% extends 'filetracking/filetracking.html' %} +{% load static %} + +{% block filetracking_tab %} + + +
+ + + + + + + + + + + + + {% for f in out_files %} + + + + + + + + {% endfor %} + + + +
File IDSent ToSubjectLast SentView File
{{f.uploader_department}}-{{f.upload_date.year}}-{{f.upload_date.month}}-#{{f.id}}{{f.sent_to_user}}-{{f.sent_to_design}}{{f.subject|default:"None"}}{{f.last_sent_date}} + +
+
+ +{% endblock %} diff --git a/FusionIIIT/templates/filetracking/outward.html b/FusionIIIT/templates/filetracking/outward.html index 73c3082f6..2666672e2 100644 --- a/FusionIIIT/templates/filetracking/outward.html +++ b/FusionIIIT/templates/filetracking/outward.html @@ -16,7 +16,7 @@

{% for designation in designation %} - {% endfor %} diff --git a/FusionIIIT/templates/filetracking/viewfile.html b/FusionIIIT/templates/filetracking/viewfile.html new file mode 100644 index 000000000..b2bd29c95 --- /dev/null +++ b/FusionIIIT/templates/filetracking/viewfile.html @@ -0,0 +1,144 @@ +{% extends 'filetracking/filetracking.html' %} +{% load static %} + +{% block filetracking_tab %} + + + +
+ +
+ {% csrf_token %} +
+
+
Subject - {{file.subject}}
+
+
+
+
+
{{file.description}}
+
+
+ {% for t in track %} +
+
+
+ {{ t.current_design }} + + + +
+ Received by: {{ t.receiver_id }}-{{ t.receive_design }} +
+ +
+
+                                            {% if t.remarks %}
+                                                {{ t.remarks }}
+                                            {% else %}
+                                                No Remarks
+                                            {% endif %}
+                                            
+ {% if t.upload_file %} + Attachment: {{ t.upload_file }} + {% else %} + No attachments + {% endif %} +
+
+
+
+ {% endfor %} + +
+ + + + {% if not file.is_read %} + + {% else %} + + {% endif %} + + + +
+
+ +
+
+
+



+
+
+
+ + + + +{% endblock %} + diff --git a/docker-compose.yml b/docker-compose.yml index e353d1423..18440f1a1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,9 +4,9 @@ services: image: postgres:13-alpine restart: always volumes: - - /private/var/lib/postgresql:/var/lib/postgresql/data + - ~/private/var/lib/postgresql:/var/lib/postgresql/data environment: - - PGDATA=/var/lib/postgresql/data/some_name/ + - PGDATA=/var/lib/postgresql/data/some_name/ - POSTGRES_DB=fusionlab - POSTGRES_USER=fusion_admin - POSTGRES_PASSWORD=hello123 diff --git a/requirements.txt b/requirements.txt index 4cc3a0a30..1898a8177 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ amqp==5.0.2 -arabic-reshaper==2.1.1 +arabic-reshaper>=2.1.1 asgiref==3.3.1 attrs==20.3.0 beautifulsoup4==4.9.3 @@ -71,4 +71,4 @@ whitenoise==5.2.0 xhtml2pdf==0.2.5 xlrd==2.0.1 XlsxWriter==1.3.7 -xlwt==1.3.0 \ No newline at end of file +xlwt==1.3.0