app.memos.views

Memo views for creation, listing, retrieval and update operations.

API Endpoints:

  • POST /api//create-memo/ : Create a memo & return first snapshot
  • GET /api//memos/ : List latest snapshot for each memo (role-based categories)
  • GET /api//memos// : Retrieve latest MemoSnapshot
  • PATCH /api//memos// : Log MemoEvent('updated') then return new snapshot
  • POST /api//memos//approve/ : Approve a memo
  • POST /api//memos//reject/ : Reject a memo
  • POST /api//memos//work-status/ : Update memo work status (attended, completed, etc)
  • DELETE /api//memos//work-status/ : Delete memo work status
  • GET/POST/PUT /api//memos//attendee-eta/ : Manage attendee ETA for a memo
  • POST /api//memos//refresh-otp/ : Refresh and return attendee/completion OTP for a memo
  • GET /api//dashboard-memos/ : Dashboard memos with date filtering (date, week, month)
  • GET /api/export/memo// : Export a single memo's latest snapshot to Excel
  • GET /api/export/memos// : Export all memos for a hospital to Excel with optional date filtering

Each endpoint is documented with drf-spectacular's @extend_schema for OpenAPI generation.

  1"""Memo views for creation, listing, retrieval and update operations.
  2
  3API Endpoints:
  4- POST   /api/<hospital_id>/create-memo/                  : Create a memo & return first snapshot
  5- GET    /api/<hospital_id>/memos/                        : List latest snapshot for each memo (role-based categories)
  6- GET    /api/<hospital_id>/memos/<uuid:memoId>/          : Retrieve latest MemoSnapshot
  7- PATCH  /api/<hospital_id>/memos/<uuid:memoId>/          : Log MemoEvent('updated') then return new snapshot
  8- POST   /api/<hospital_id>/memos/<uuid:memoId>/approve/  : Approve a memo
  9- POST   /api/<hospital_id>/memos/<uuid:memoId>/reject/   : Reject a memo
 10- POST   /api/<hospital_id>/memos/<uuid:memoId>/work-status/ : Update memo work status (attended, completed, etc)
 11- DELETE /api/<hospital_id>/memos/<uuid:memoId>/work-status/ : Delete memo work status
 12- GET/POST/PUT /api/<hospital_id>/memos/<uuid:memoId>/attendee-eta/ : Manage attendee ETA for a memo
 13- POST   /api/<hospital_id>/memos/<uuid:memoId>/refresh-otp/ : Refresh and return attendee/completion OTP for a memo
 14- GET    /api/<hospital_id>/dashboard-memos/              : Dashboard memos with date filtering (date, week, month)
 15- GET    /api/export/memo/<memo_id>/                      : Export a single memo's latest snapshot to Excel
 16- GET    /api/export/memos/<hospital_id>/                 : Export all memos for a hospital to Excel with optional date filtering
 17
 18Each endpoint is documented with drf-spectacular's @extend_schema for OpenAPI generation.
 19"""
 20
 21from django.shortcuts import get_object_or_404
 22from rest_framework import generics, permissions, status
 23from rest_framework.response import Response
 24from rest_framework.views import APIView
 25from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse
 26from django.http import Http404
 27from accounts.models import Hospital
 28from .models import Memo, MemoEvents, MemoSnapshot
 29from .serializers import (
 30    MemoSnapshotSerializer,
 31    MemoSerializer,
 32    MemoCreateSerializer,
 33)
 34import calendar
 35from datetime import datetime,timedelta
 36from django.utils import timezone
 37from django.db.models import Q, OuterRef, Subquery, Exists
 38from rest_framework.response import Response
 39from rest_framework import generics, permissions
 40from django.shortcuts import get_object_or_404
 41from django.http import StreamingHttpResponse
 42from datetime import datetime, timedelta
 43import pandas as pd
 44import io
 45
 46@extend_schema(
 47    summary="Create a memo",
 48    description="Create a memo and respond with its first snapshot.",
 49    request=MemoCreateSerializer,
 50    responses={201: MemoSnapshotSerializer}
 51)
 52class CreateMemoView(APIView):
 53    """Create a memo and respond with its first snapshot."""
 54
 55    permission_classes = [permissions.IsAuthenticated]
 56
 57    def post(self, request, hospital_id):
 58        hospital = get_object_or_404(Hospital, pk=hospital_id)
 59        data = request.data.copy()
 60        data["hospital"] = hospital.id
 61
 62        serializer = MemoCreateSerializer(data=data, context={"request": request})
 63        serializer.is_valid(raise_exception=True)
 64        memo = serializer.save()
 65
 66        snapshot = memo.snapshots.order_by("-timestamp").first()
 67        return Response(
 68            MemoSnapshotSerializer(snapshot).data,
 69            status=status.HTTP_201_CREATED,
 70        )
 71
 72
 73@extend_schema(
 74    summary="List memos for user",
 75    description="Return structured memo categories depending on user role.",
 76    responses={200: MemoSerializer(many=True)}
 77)
 78class ListUserMemosView(generics.GenericAPIView):
 79    """
 80    Return structured memo categories depending on user role.
 81    Categories: creator, approver, responder, admin.
 82    """
 83    permission_classes = [permissions.IsAuthenticated]
 84    serializer_class = MemoSerializer
 85
 86    def get_serializer_context(self):
 87        context = super().get_serializer_context()
 88        context["hospital_id"] = self.kwargs["hospital_id"]
 89        return context
 90
 91    def get_creator_memos(self, user):
 92        data = {'creator': {}}
 93        try:
 94            data['creator']['My Memos'] = MemoSerializer(
 95                MemoSnapshot.objects.filter(
 96                    info__user_id=user.id, info__status="ongoing"
 97                ).order_by("-timestamp"), many=True
 98            ).data
 99        except Exception:
100            data['creator']['My Memos'] = []
101
102        try:
103            data['creator']['Local Memos'] = MemoSerializer(
104                MemoSnapshot.objects.filter(
105                    info__current_ward_id=user.current_ward.id
106                ).order_by("-timestamp"), many=True
107            ).data
108        except Exception:
109            data['creator']['Local Memos'] = []
110
111        try:
112            data['creator']['Completed'] = MemoSerializer(
113                MemoSnapshot.objects.filter(
114                    info__user_id=user.id, info__status="completed"
115                ).order_by("-timestamp"), many=True
116            ).data
117        except Exception:
118            data['creator']['Completed'] = []
119
120        return data
121
122    def get_approver_memos(self, user):
123        role_id = user.role.id
124        key = f"{role_id}_approved"
125        data = {'approver': {}}
126
127        try:
128            waiting_approval = MemoSnapshot.objects.filter(
129                **{f"approval_history__{key}": False}
130            ).order_by("-timestamp")
131
132            data['approver']['Under Review'] = MemoSerializer(
133                waiting_approval, many=True
134            ).data
135        except Exception:
136            data['approver']['Under Review'] = []
137
138        try:
139            user_id = user.id
140
141            latest_approval_time = MemoEvents.objects.filter(
142                event_type='approved',
143                event_by_id=user_id,
144                memo_id=OuterRef('memo_id')
145            ).order_by('-event_timestamp').values('event_timestamp')[:1]
146
147            rejected_after = MemoEvents.objects.filter(
148                event_type='rejected',
149                event_by_id=user_id,
150                memo_id=OuterRef('memo_id'),
151                event_timestamp__gt=Subquery(latest_approval_time)
152            )
153
154            memos_user_approved_and_not_rejected = MemoEvents.objects.filter(
155                event_type='approved',
156                event_by_id=user_id
157            ).annotate(
158                rejected_after=Exists(rejected_after)
159            ).filter(
160                Q(rejected_after=False)
161            ).values_list('memo_id', flat=True).distinct()
162
163            previous_blocks = MemoSnapshot.objects.filter(
164                memo_id__in=memos_user_approved_and_not_rejected
165            ).order_by("-timestamp")
166
167            completed = previous_blocks.filter(
168                info__status="completed"
169            ).order_by("-timestamp")
170
171            previous_blocks = previous_blocks.exclude(
172                memo_id__in=completed.values_list('memo_id', flat=True)
173            ).order_by("-timestamp")
174
175            data['approver']['Reviewed'] = MemoSerializer(
176                previous_blocks, many=True
177            ).data
178            data['approver']['completed'] = MemoSerializer(
179                completed, many=True
180            ).data
181        except Exception:
182            data['approver']['Reviewed'] = []
183            data['approver']['completed'] = []
184
185        return data
186
187    def get_responder_memos(self, user):
188        data = {'responder': {}}
189
190        try:
191            data['responder']['Open Memos'] = MemoSerializer(
192                MemoSnapshot.objects.filter(
193                    worker_status=[],
194                    info__tagged_role_id=str(user.role.id)
195                ).order_by("-timestamp"),
196                many=True
197            ).data
198        except Exception:
199            data['responder']['Open Memos'] = []
200            
201        try:
202            latest_attended = MemoEvents.objects.filter(
203                event_type='attended', event_by=user
204            )
205
206            completed_after = MemoEvents.objects.filter(
207                event_type='completed',
208                event_by=user,
209                memo_id=OuterRef('memo_id'),
210                event_timestamp__gt=OuterRef('event_timestamp')
211            )
212
213            attending_memo_ids = latest_attended.annotate(
214                has_completed=Exists(completed_after)
215            ).filter(
216                has_completed=False
217            ).values_list('memo_id', flat=True)
218
219            data['responder']['Attending'] = MemoSerializer(
220                MemoSnapshot.objects.filter(
221                    memo_id__in=attending_memo_ids
222                ).order_by("-timestamp"),
223                many=True
224            ).data
225        except Exception:
226            data['responder']['Attending'] = []
227
228        try:
229            completed_memo_ids = MemoEvents.objects.filter(
230                event_type='completed', event_by=user
231            ).values_list('memo_id', flat=True)
232
233            data['responder']['completed'] = MemoSerializer(
234                MemoSnapshot.objects.filter(
235                    memo_id__in=completed_memo_ids
236                ).order_by("-timestamp"),
237                many=True
238            ).data
239        except Exception:
240            data['responder']['completed'] = []
241
242
243        try:
244            active_work_memo_ids = MemoEvents.objects.filter(
245                event_type__in=['attended', 'completed', 'incomplete'],
246                event_by=user
247            ).values_list('memo_id', flat=True)
248
249            tagged_memo_ids = MemoEvents.objects.filter(
250                event_type='tagged',
251                payload__tagged_role_id=str(user.role.id)
252            ).exclude(
253                memo_id__in=active_work_memo_ids
254            ).values_list('memo_id', flat=True)
255
256            data['responder']['Tagged Memos'] = MemoSerializer(
257                MemoSnapshot.objects.filter(
258                    memo_id__in=tagged_memo_ids
259                ).order_by("-timestamp"),
260                many=True
261            ).data
262        except Exception:
263            data['responder']['Tagged Memos'] = []
264
265        return data
266
267    def get_admin_memos(self, user):
268        """
269        Return memos for admins:
270        - 'Pending Approval': Memos not completed and waiting for approval at any level.
271        - 'Completed': Memos approved at all levels and completed.
272        """
273        data = {'admin': {}}
274        data['admin']['all'] = MemoSerializer(MemoSnapshot.objects.all(),many=True).data
275        try:
276            # Memos not completed and waiting for approval (assuming approval_history tracks all levels)
277            pending_approval = MemoSnapshot.objects.filter(
278                memo__hospital=user.hospital,
279                info__status="ongoing",  # adjust as per your status values
280                memo__is_deleted=False
281            ).order_by("-timestamp")
282
283            data['admin']['Pending Approval'] = MemoSerializer(
284                pending_approval, many=True
285            ).data
286        except Exception:
287            data['admin']['Pending Approval'] = []
288
289        try:
290            # Memos completed and approved at all levels
291            completed = MemoSnapshot.objects.filter(
292                memo__hospital=user.hospital,
293                info__status="completed",
294                memo__is_deleted=False
295            ).order_by("-timestamp")
296
297            data['admin']['Completed'] = MemoSerializer(
298                completed, many=True
299            ).data
300        except Exception:
301            data['admin']['Completed'] = []
302        try:
303            # Memos completed and approved at all levels
304            deleted = MemoSnapshot.objects.filter(
305                memo__hospital=user.hospital,
306                memo__is_deleted=True,
307            ).order_by("-timestamp")
308
309            data['admin']['Deleted Memos'] = MemoSerializer(
310                deleted, many=True
311            ).data
312        except Exception:
313            data['admin']['Deleted Memos'] = []
314
315        return data
316
317    def list(self, request, *args, **kwargs):
318        user = request.user
319        role = user.role
320
321        if user.is_superuser or user.is_staff:
322            data = self.get_admin_memos(user)
323        elif role.is_creator:
324            data = self.get_creator_memos(user)
325        elif role.is_approver:
326            data = self.get_approver_memos(user)
327        elif role.is_responder:
328            data = self.get_responder_memos(user)
329        else:
330            data = {"detail": "Unsupported role"}
331
332        return Response(data)
333
334    def get(self, request, *args, **kwargs):
335        return self.list(request, *args, **kwargs)
336
337
338
339@extend_schema(
340    summary="Retrieve or update memo snapshot",
341    description="GET: Retrieve latest snapshot for a memo. PATCH: Log update event and return new snapshot.",
342    responses={200: MemoSnapshotSerializer}
343)
344class MemoDetailView(generics.RetrieveAPIView):
345    """Retrieve latest snapshot or update memo via event logging."""
346    serializer_class = MemoSnapshotSerializer
347    lookup_field = "memoId"
348    permission_classes = [permissions.IsAuthenticated]
349
350    def get_object(self):
351        """Return the latest snapshot for the requested memo."""
352        hospital_id = self.kwargs["hospital_id"]
353        memo_id = self.kwargs["memoId"]
354        memo = get_object_or_404(
355            Memo,
356            # hospital_id=hospital_id,
357            memoId=memo_id,
358            is_deleted=False,
359        )
360        snapshot = memo.snapshots.order_by("-timestamp").first()
361        if not snapshot:
362            raise Http404("No snapshots found for this memo.")
363        return snapshot
364
365    def patch(self, request, *args, **kwargs):
366        hospital = get_object_or_404(Hospital, pk=kwargs["hospital_id"])
367        memo = get_object_or_404(
368            Memo,
369            hospital=hospital,
370            memoId=kwargs["memoId"],
371            is_deleted=False,
372        )
373
374        # Log the update event; post_save signal will create a new snapshot
375        MemoEvents.objects.create(
376            memo=memo,
377            event_type="updated",
378            event_by=request.user,
379            payload=request.data.get("payload", {}),
380            metadata=request.data.get("metadata", {}),
381        )
382
383        # Fetch and return the newly created snapshot
384        snapshot = memo.snapshots.order_by("-timestamp").first()
385        return Response(
386            MemoSnapshotSerializer(snapshot).data,
387            status=status.HTTP_200_OK,
388        )
389
390
391@extend_schema(
392    summary="Approve a memo",
393    description="Approve a memo and log the event.",
394    request=None,
395    responses={200: OpenApiResponse(description="Memo approved successfully.")}
396)
397class MemoApproveView(APIView):
398    """Approve a memo and log the event."""
399
400    permission_classes = [permissions.IsAuthenticated]
401
402    def post(self, request, hospital_id, memoId):
403        hospital = get_object_or_404(Hospital, pk=hospital_id)
404        memo = get_object_or_404(Memo,
405            hospital=hospital,
406            memoId=memoId,
407            is_deleted=False,
408        )
409
410        # Log the approval event
411        MemoEvents.objects.create(
412            memo=memo,
413            event_type="approved",
414            event_by=request.user,
415            payload=request.data.get("payload", {}),
416            metadata=request.data.get("metadata", {}),
417        )
418
419        return Response(
420            {"message": "Memo approved successf ully."},
421            status=status.HTTP_200_OK,
422        )
423        
424@extend_schema(
425    summary="Reject a memo",
426    description="Reject a memo and log the event.",
427    request=None,
428    responses={200: OpenApiResponse(description="Memo rejected successfully.")}
429)
430class MemoRejectView(APIView):
431    """Reject a memo and log the event."""
432
433    permission_classes = [permissions.IsAuthenticated]
434
435    def post(self, request, hospital_id, memoId):
436        hospital = get_object_or_404(Hospital, pk=hospital_id)
437        memo = get_object_or_404(Memo,
438            hospital=hospital,
439            memoId=memoId,
440            is_deleted=False,
441        )
442
443        # Log the approval event
444        MemoEvents.objects.create(
445            memo=memo,
446            event_type="rejected",
447            event_by=request.user,
448            payload=request.data.get("payload", {}),
449            metadata=request.data.get("metadata", {}),
450        )
451
452        return Response(
453            {"message": "Memo rejected successfully."},
454            status=status.HTTP_200_OK,
455        )
456        
457
458# to implement worker status updates and deletion of their works .. 
459
460"""
461idea is events for their work status and id of the event of their work status
462event : comment , tag user , type
463snapshot comment , tag user , type , event id 
464
465after on delete of work status -> eventid -> filter in the snapshot and mark it as deleted : True
466
467""" 
468
469from .models import AttendeeETA
470from .serializers import AttendeeETASerializer
471from rest_framework.permissions import IsAuthenticated
472from rest_framework import status
473
474@extend_schema(
475    summary="Create, update, or list attendee ETA for a memo",
476    description="GET: List all attendee ETAs for a memo. POST: Create ETA. PUT: Update ETA for current user.",
477    request=AttendeeETASerializer,
478    responses={200: AttendeeETASerializer(many=True), 201: AttendeeETASerializer}
479)
480class AttendeeETACreateView(APIView):
481    """Manage attendee ETA for a memo."""
482    permission_classes = [IsAuthenticated]
483
484    def get(self, request, hospital_id, memoId):
485        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
486        # Return list of all attendee ETAs for this memo
487        etas = AttendeeETA.objects.filter(memo=memo)
488        serializer = AttendeeETASerializer(etas, many=True)
489        return Response(serializer.data, status=status.HTTP_200_OK)
490
491    def post(self, request, hospital_id, memoId):
492        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
493        data = request.data.copy()
494        data['memo'] = memo.memoId
495        data['attendee'] = request.user.id
496        serializer = AttendeeETASerializer(data=data)
497        if serializer.is_valid():
498            serializer.save()
499            return Response(serializer.data, status=status.HTTP_201_CREATED)
500        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
501
502    def put(self, request, hospital_id, memoId):
503        """Update current user's ETA for this memo"""
504        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
505        try:
506            attendee_eta = AttendeeETA.objects.get(memo=memo, attendee=request.user)
507        except AttendeeETA.DoesNotExist:
508            return Response({"detail": "ETA not found for current user."}, status=status.HTTP_404_NOT_FOUND)
509
510        serializer = AttendeeETASerializer(attendee_eta, data=request.data, partial=True)
511        if serializer.is_valid():
512            serializer.save()
513            return Response(serializer.data, status=status.HTTP_200_OK)
514        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
515
516
517@extend_schema(
518    summary="Update memo work status",
519    description="POST: Update memo work status (attended, completed, etc). DELETE: Delete work status.",
520    request=None,
521    responses={200: OpenApiResponse(description="Memo work status updated/deleted successfully.")}
522)
523class MemoWorkStatusView(APIView):
524    """Update or delete memo work status."""
525
526    permission_classes = [permissions.IsAuthenticated]
527
528    def post(self, request, hospital_id, memoId):
529        hospital = get_object_or_404(Hospital, pk=hospital_id)
530        memo = get_object_or_404(Memo,
531            hospital=hospital,
532            memoId=memoId,
533            is_deleted=False,
534        )
535        
536        payload = request.data.get('payload')
537        otp =  payload.get("otp")
538        if otp and request.data.get('event_type') in ['attended','incomplete','tagged']:
539            if memo.latest_snapshot().attendee_otp == otp:
540                pass
541            else:
542                return Response({"isValid":False},status.HTTP_202_ACCEPTED) 
543        
544        if otp and request.data.get('event_type') == "completed":
545            if memo.latest_snapshot().completion_otp == otp:
546                pass
547            else:
548                print("otp mismatch")
549                return Response({"isValid":False},status.HTTP_202_ACCEPTED) 
550
551        # Log the approval event
552        MemoEvents.objects.create(
553            memo=memo,
554            event_type=request.data.get("event_type", "attended"),
555            event_by=request.user,
556            payload=request.data.get("payload", {}),
557            metadata=request.data.get("metadata", {}),
558        )
559
560        return Response(
561            {"message": "Memo work status updated successfully.","isValid":True},
562            status=status.HTTP_200_OK,
563        )
564        
565    def delete(self, request, hospital_id, memoId):
566        hospital = get_object_or_404(Hospital, pk=hospital_id)
567        memo = get_object_or_404(Memo,
568            hospital=hospital,
569            memoId=memoId,
570            is_deleted=False,
571        )
572
573        # Log the approval event
574        MemoEvents.objects.create(
575            memo=memo,
576            event_type="worker_status_delete",
577            event_by=request.user,
578            payload=request.data.get("payload", {}),
579            metadata=request.data.get("metadata", {}),
580        )
581
582        return Response(
583            {"message": "Memo work status deleted successfully."},
584            status=status.HTTP_200_OK,
585        )
586
587
588@extend_schema(
589    summary="Refresh OTP for memo",
590    description="Refresh and return attendee/completion OTP for a memo.",
591    request=None,
592    responses={200: OpenApiResponse(description="OTP refreshed successfully.")}
593)
594class RefreshOTPView(APIView):
595    """Refresh and return attendee/completion OTP for a memo."""
596
597    permission_classes = [permissions.IsAuthenticated]
598
599    def post(self, request, hospital_id, memoId):
600        hospital = get_object_or_404(Hospital, pk=hospital_id)
601        memo = get_object_or_404(Memo,
602            hospital=hospital,
603            memoId=memoId,
604            is_deleted=False,
605        )
606            
607        snapshot = memo.latest_snapshot()
608        if snapshot:
609            attendee_otp = snapshot.generate_otp()
610            # completion_otp = snapshot.generate_otp()
611            
612            snapshot.attendee_otp = attendee_otp
613            # snapshot.completion_otp = completion_otp
614            
615            snapshot.save()
616            
617            return Response({
618                "attendee_otp":attendee_otp,
619                "completion_otp":snapshot.completion_otp
620            },status=status.HTTP_200_OK)
621            
622        else:
623            return Response({"message":"some error"},status=status.HTTP_404_NOT_FOUND)
624        
625        
626    
627@extend_schema(
628    summary="Dashboard memos with date filtering",
629    description="Return structured memo categories depending on user role with date filtering (date, week, month).",
630    parameters=[
631        OpenApiParameter(name="period_type", type=str, location="query", required=False, description="Type of period filter: date, week, month"),
632        OpenApiParameter(name="value", type=str, location="query", required=False, description="Date value (YYYY-MM-DD)"),
633        OpenApiParameter(name="year", type=int, location="query", required=False, description="Year for week/month filter"),
634        OpenApiParameter(name="week", type=int, location="query", required=False, description="Week number for week filter"),
635        OpenApiParameter(name="month", type=int, location="query", required=False, description="Month number for month filter"),
636    ],
637    responses={200: MemoSnapshotSerializer(many=True)}
638)
639class DashboardMemosView(generics.GenericAPIView):
640    """
641    Return structured memo categories depending on user role with date filtering.
642    Supports filtering by date, week, or month based on memo creation time.
643    """
644    permission_classes = [permissions.IsAuthenticated]
645    serializer_class = MemoSnapshotSerializer
646
647    def get_serializer_context(self):
648        context = super().get_serializer_context()
649        context["hospital_id"] = self.kwargs["hospital_id"]
650        return context
651    
652    def get_date_filter(self, request):
653        """
654        Parse date filtering parameters from request and return Q object for filtering.
655        Expected parameters:
656        - period_type: 'date', 'week', or 'month'
657        - For 'date': value (YYYY-MM-DD format)
658        - For 'week': year, week
659        - For 'month': year, month
660        """
661        period_type = request.query_params.get('period_type', 'month')
662        
663        if period_type == 'date':
664            # Filter by specific date
665            date_value = request.query_params.get('value')
666            if date_value:
667                try:
668                    target_date = datetime.strptime(date_value, '%Y-%m-%d').date()
669                    start_datetime = timezone.make_aware(
670                        datetime.combine(target_date, datetime.min.time())
671                    )
672                    end_datetime = start_datetime + timedelta(days=1)
673                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
674                except ValueError:
675                    # Invalid date format, return no filter
676                    pass
677        
678        elif period_type == 'week':
679            # Filter by specific week
680            year = request.query_params.get('year')
681            week = request.query_params.get('week')
682            if year and week:
683                try:
684                    year = int(year)
685                    week = int(week)
686                    
687                    # Calculate the start of the week (Monday)
688                    # ISO week date calculation
689                    jan_1 = datetime(year, 1, 1)
690                    week_start = jan_1 + timedelta(days=(week - 1) * 7 - jan_1.weekday())
691                    week_end = week_start + timedelta(days=7)
692                    
693                    start_datetime = timezone.make_aware(week_start)
694                    end_datetime = timezone.make_aware(week_end)
695                    
696                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
697                except (ValueError, TypeError):
698                    # Invalid year/week format, return no filter
699                    pass
700        
701        elif period_type == 'month':
702            # Filter by specific month
703            year = request.query_params.get('year')
704            month = request.query_params.get('month')
705            if year and month:
706                try:
707                    year = int(year)
708                    month = int(month)
709                    
710                    # Get first day of the month
711                    start_date = datetime(year, month, 1)
712                    # Get first day of next month (or next year if December)
713                    if month == 12:
714                        end_date = datetime(year + 1, 1, 1)
715                    else:
716                        end_date = datetime(year, month + 1, 1)
717                    
718                    start_datetime = timezone.make_aware(start_date)
719                    end_datetime = timezone.make_aware(end_date)
720                    
721                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
722                except (ValueError, TypeError):
723                    # Invalid year/month format, return no filter
724                    pass
725        
726        # Default: return all records (no date filter)
727        return Q()
728    
729    def get_queryset(self):
730        """
731        Get filtered queryset based on hospital and date parameters.
732        """
733        h_id = self.kwargs["hospital_id"]
734        date_filter = self.get_date_filter(self.request)
735        
736        # Get memos for the hospital with date filtering
737        memos = Memo.objects.filter(
738            hospital__id=h_id,
739            is_deleted=False  # Exclude deleted memos
740        ).filter(date_filter)
741        
742        return memos
743    
744    def get(self, request, *args, **kwargs):
745        """
746        Return memos with their latest snapshots for the specified hospital and date range.
747        """
748        try:
749            memos = self.get_queryset()
750            memos_snapshots = [memo.latest_snapshot() for memo in memos]
751            # Build response data with memo and latest snapshot info
752            memo_data = MemoSerializer(memos_snapshots,many=True).data
753            return Response({
754                'count': len(memo_data),
755                'memos': memo_data,
756                'filters': {
757                    'period_type': request.query_params.get('period_type', 'month'),
758                    'hospital_id': kwargs["hospital_id"]
759                }
760            }, status=status.HTTP_200_OK)
761            
762        except Exception as e:
763            return Response({
764                'error': 'Failed to fetch memos',
765                'detail': str(e)
766            }, status=status.HTTP_400_BAD_REQUEST)
767
768
769# Helper function to create excel
770def format_memo_row(memo):
771    info = memo.get("info", {})
772    hierarchy = memo.get("hierarchy", [])
773    approval_history = memo.get("approval_history", {})
774    worker_status = memo.get("worker_status", [])
775
776    row = {
777        "Memo Link": f"https://web.memotrack.net/memos/{memo['memo']}",
778        "Complaint": info.get("complaint", ""),
779        "Raised By": f"{info.get('user_role_name', '')} ({info.get('phone_number', '')})",
780        "Ward + Block + Floor": f"{info.get('current_ward_name', '')}, {info.get('current_block_name', '')}, Floor {info.get('current_floor', '')}",
781        "Time": memo.get("created_at", "")
782    }
783
784    # Add Approver details
785    for idx, approver in enumerate(hierarchy, start=1):
786        role_id = approver["role"]
787        approved_key = f"{role_id}_approved"
788        # Fetching approved status and time from the hierarchy list
789        approved = approver.get("approved", False)
790        approved_time = approver.get("approved_at", "")
791
792
793        row[f"Approver_{idx}_Role"] = approver.get("role_name", "")
794        row[f"Approver_{idx}_Approved"] = "Yes" if approved else "No"
795        row[f"Approver_{idx}_Time"] = approved_time if approved else ""
796        row[f"Approver_{idx}_Phone"] = approver.get("by_phone", "")
797        row[f"Approver_{idx}_Name"] = approver.get("approved_by", "")
798
799    # Worker status comments
800    worker_lines = []
801    for status in worker_status:
802        line = f"{status.get('by_institution_id', '')} ({status.get('by_phone', '')}) at {status.get('timestamp', '')}:\n{status.get('comment', '')}"
803        worker_lines.append(line)
804    row["Worker Status Comments"] = "\n\n".join(worker_lines)
805
806    return row
807
808
809
810@extend_schema(
811    summary="Export single memo to Excel",
812    description="Export a single memo's latest snapshot to Excel.",
813    parameters=[
814        OpenApiParameter(name="memo_id", type=str, location="path", required=True, description="Memo ID"),
815    ],
816    responses={200: OpenApiResponse(description="Excel file with memo data")}
817)
818class ExportSingleMemoExcelView(APIView):
819    permission_classes = [permissions.AllowAny]
820
821    def get(self, request, memo_id):
822        try:
823            memo = Memo.objects.get(memoId=memo_id, is_deleted=False)
824            snapshot = MemoSnapshotSerializer(memo.latest_snapshot()).data
825
826            # Format single row
827            row = format_memo_row(snapshot)
828            df = pd.DataFrame([row])
829
830            output = io.BytesIO()
831            with pd.ExcelWriter(output, engine="openpyxl") as writer:
832                df.to_excel(writer, index=False)
833
834            output.seek(0)
835            filename = f"memo_{memo.memoId}_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
836
837            response = StreamingHttpResponse(
838                output,
839                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
840            )
841            response['Content-Disposition'] = f'attachment; filename="{filename}"'
842            return response
843
844        except Memo.DoesNotExist:
845            raise Http404("Memo not found")
846        except Exception as e:
847            return StreamingHttpResponse(
848                content=f"Failed to export: {str(e)}",
849                status=400,
850                content_type='text/plain'
851            )
852
853@extend_schema(
854    summary="Export memos to Excel",
855    description="Export all memos for a hospital to Excel with optional date filtering.",
856    parameters=[
857        OpenApiParameter(name="hospital_id", type=int, location="path", required=True, description="Hospital ID"),
858        OpenApiParameter(name="period_type", type=str, location="query", required=False, description="Type of period filter: date, week, month"),
859        OpenApiParameter(name="value", type=str, location="query", required=False, description="Date value (YYYY-MM-DD)"),
860        OpenApiParameter(name="year", type=int, location="query", required=False, description="Year for week/month filter"),
861        OpenApiParameter(name="week", type=int, location="query", required=False, description="Week number for week filter"),
862        OpenApiParameter(name="month", type=int, location="query", required=False, description="Month number for month filter"),
863    ],
864    responses={200: OpenApiResponse(description="Excel file with memos data")}
865)
866class ExportMemosExcelView(APIView):
867    """Export all memos for a hospital to Excel with optional date filtering."""
868    permission_classes = [permissions.AllowAny]
869
870    def get_date_filter(self, request):
871        period_type = request.query_params.get('period_type', 'month')
872
873        if period_type == 'date':
874            date_value = request.query_params.get('value')
875            if date_value:
876                try:
877                    target_date = datetime.strptime(date_value, '%Y-%m-%d').date()
878                    start = timezone.make_aware(datetime.combine(target_date, datetime.min.time()))
879                    end = start + timedelta(days=1)
880                    return Q(created_at__gte=start, created_at__lt=end)
881                except ValueError:
882                    pass
883
884        elif period_type == 'week':
885            year = request.query_params.get('year')
886            week = request.query_params.get('week')
887            if year and week:
888                try:
889                    year, week = int(year), int(week)
890                    jan_4 = datetime(year, 1, 4)
891                    start = jan_4 + timedelta(weeks=week-1, days=-jan_4.weekday())
892                    end = start + timedelta(days=7)
893                    return Q(created_at__gte=timezone.make_aware(start), created_at__lt=timezone.make_aware(end))
894                except (ValueError, TypeError):
895                    pass
896
897        elif period_type == 'month':
898            year = request.query_params.get('year')
899            month = request.query_params.get('month')
900            if year and month:
901                try:
902                    year, month = int(year), int(month)
903                    start = timezone.make_aware(datetime(year, month, 1))
904                    if month == 12:
905                        end = timezone.make_aware(datetime(year + 1, 1, 1))
906                    else:
907                        end = timezone.make_aware(datetime(year, month + 1, 1))
908                    return Q(created_at__gte=start, created_at__lt=end)
909                except (ValueError, TypeError):
910                    pass
911
912        return Q()
913
914    def get(self, request, hospital_id):
915        try:
916            date_filter = self.get_date_filter(request)
917            memos = Memo.objects.filter(hospital__id=hospital_id, is_deleted=False).filter(date_filter)
918            snapshots = [MemoSnapshotSerializer(memo.latest_snapshot()).data for memo in memos]
919
920            rows = [format_memo_row(snapshot) for snapshot in snapshots]
921            df = pd.DataFrame(rows)
922
923            output = io.BytesIO()
924            with pd.ExcelWriter(output, engine="openpyxl") as writer:
925                df.to_excel(writer, index=False)
926
927            output.seek(0)
928            filename = f"memo_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
929
930            response = StreamingHttpResponse(
931                output,
932                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
933            )
934            response['Content-Disposition'] = f'attachment; filename="{filename}"'
935            return response
936
937        except Exception as e:
938            return StreamingHttpResponse(
939                content=f"Failed to export: {str(e)}",
940                status=400,
941                content_type='text/plain'
942            )
@extend_schema(summary='Create a memo', description='Create a memo and respond with its first snapshot.', request=MemoCreateSerializer, responses={201: MemoSnapshotSerializer})
class CreateMemoView(rest_framework.views.APIView):
47@extend_schema(
48    summary="Create a memo",
49    description="Create a memo and respond with its first snapshot.",
50    request=MemoCreateSerializer,
51    responses={201: MemoSnapshotSerializer}
52)
53class CreateMemoView(APIView):
54    """Create a memo and respond with its first snapshot."""
55
56    permission_classes = [permissions.IsAuthenticated]
57
58    def post(self, request, hospital_id):
59        hospital = get_object_or_404(Hospital, pk=hospital_id)
60        data = request.data.copy()
61        data["hospital"] = hospital.id
62
63        serializer = MemoCreateSerializer(data=data, context={"request": request})
64        serializer.is_valid(raise_exception=True)
65        memo = serializer.save()
66
67        snapshot = memo.snapshots.order_by("-timestamp").first()
68        return Response(
69            MemoSnapshotSerializer(snapshot).data,
70            status=status.HTTP_201_CREATED,
71        )

Create a memo and respond with its first snapshot.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def post(self, request, hospital_id):
58    def post(self, request, hospital_id):
59        hospital = get_object_or_404(Hospital, pk=hospital_id)
60        data = request.data.copy()
61        data["hospital"] = hospital.id
62
63        serializer = MemoCreateSerializer(data=data, context={"request": request})
64        serializer.is_valid(raise_exception=True)
65        memo = serializer.save()
66
67        snapshot = memo.snapshots.order_by("-timestamp").first()
68        return Response(
69            MemoSnapshotSerializer(snapshot).data,
70            status=status.HTTP_201_CREATED,
71        )
schema
@extend_schema(summary='List memos for user', description='Return structured memo categories depending on user role.', responses={200: MemoSerializer(many=True)})
class ListUserMemosView(rest_framework.generics.GenericAPIView):
 74@extend_schema(
 75    summary="List memos for user",
 76    description="Return structured memo categories depending on user role.",
 77    responses={200: MemoSerializer(many=True)}
 78)
 79class ListUserMemosView(generics.GenericAPIView):
 80    """
 81    Return structured memo categories depending on user role.
 82    Categories: creator, approver, responder, admin.
 83    """
 84    permission_classes = [permissions.IsAuthenticated]
 85    serializer_class = MemoSerializer
 86
 87    def get_serializer_context(self):
 88        context = super().get_serializer_context()
 89        context["hospital_id"] = self.kwargs["hospital_id"]
 90        return context
 91
 92    def get_creator_memos(self, user):
 93        data = {'creator': {}}
 94        try:
 95            data['creator']['My Memos'] = MemoSerializer(
 96                MemoSnapshot.objects.filter(
 97                    info__user_id=user.id, info__status="ongoing"
 98                ).order_by("-timestamp"), many=True
 99            ).data
100        except Exception:
101            data['creator']['My Memos'] = []
102
103        try:
104            data['creator']['Local Memos'] = MemoSerializer(
105                MemoSnapshot.objects.filter(
106                    info__current_ward_id=user.current_ward.id
107                ).order_by("-timestamp"), many=True
108            ).data
109        except Exception:
110            data['creator']['Local Memos'] = []
111
112        try:
113            data['creator']['Completed'] = MemoSerializer(
114                MemoSnapshot.objects.filter(
115                    info__user_id=user.id, info__status="completed"
116                ).order_by("-timestamp"), many=True
117            ).data
118        except Exception:
119            data['creator']['Completed'] = []
120
121        return data
122
123    def get_approver_memos(self, user):
124        role_id = user.role.id
125        key = f"{role_id}_approved"
126        data = {'approver': {}}
127
128        try:
129            waiting_approval = MemoSnapshot.objects.filter(
130                **{f"approval_history__{key}": False}
131            ).order_by("-timestamp")
132
133            data['approver']['Under Review'] = MemoSerializer(
134                waiting_approval, many=True
135            ).data
136        except Exception:
137            data['approver']['Under Review'] = []
138
139        try:
140            user_id = user.id
141
142            latest_approval_time = MemoEvents.objects.filter(
143                event_type='approved',
144                event_by_id=user_id,
145                memo_id=OuterRef('memo_id')
146            ).order_by('-event_timestamp').values('event_timestamp')[:1]
147
148            rejected_after = MemoEvents.objects.filter(
149                event_type='rejected',
150                event_by_id=user_id,
151                memo_id=OuterRef('memo_id'),
152                event_timestamp__gt=Subquery(latest_approval_time)
153            )
154
155            memos_user_approved_and_not_rejected = MemoEvents.objects.filter(
156                event_type='approved',
157                event_by_id=user_id
158            ).annotate(
159                rejected_after=Exists(rejected_after)
160            ).filter(
161                Q(rejected_after=False)
162            ).values_list('memo_id', flat=True).distinct()
163
164            previous_blocks = MemoSnapshot.objects.filter(
165                memo_id__in=memos_user_approved_and_not_rejected
166            ).order_by("-timestamp")
167
168            completed = previous_blocks.filter(
169                info__status="completed"
170            ).order_by("-timestamp")
171
172            previous_blocks = previous_blocks.exclude(
173                memo_id__in=completed.values_list('memo_id', flat=True)
174            ).order_by("-timestamp")
175
176            data['approver']['Reviewed'] = MemoSerializer(
177                previous_blocks, many=True
178            ).data
179            data['approver']['completed'] = MemoSerializer(
180                completed, many=True
181            ).data
182        except Exception:
183            data['approver']['Reviewed'] = []
184            data['approver']['completed'] = []
185
186        return data
187
188    def get_responder_memos(self, user):
189        data = {'responder': {}}
190
191        try:
192            data['responder']['Open Memos'] = MemoSerializer(
193                MemoSnapshot.objects.filter(
194                    worker_status=[],
195                    info__tagged_role_id=str(user.role.id)
196                ).order_by("-timestamp"),
197                many=True
198            ).data
199        except Exception:
200            data['responder']['Open Memos'] = []
201            
202        try:
203            latest_attended = MemoEvents.objects.filter(
204                event_type='attended', event_by=user
205            )
206
207            completed_after = MemoEvents.objects.filter(
208                event_type='completed',
209                event_by=user,
210                memo_id=OuterRef('memo_id'),
211                event_timestamp__gt=OuterRef('event_timestamp')
212            )
213
214            attending_memo_ids = latest_attended.annotate(
215                has_completed=Exists(completed_after)
216            ).filter(
217                has_completed=False
218            ).values_list('memo_id', flat=True)
219
220            data['responder']['Attending'] = MemoSerializer(
221                MemoSnapshot.objects.filter(
222                    memo_id__in=attending_memo_ids
223                ).order_by("-timestamp"),
224                many=True
225            ).data
226        except Exception:
227            data['responder']['Attending'] = []
228
229        try:
230            completed_memo_ids = MemoEvents.objects.filter(
231                event_type='completed', event_by=user
232            ).values_list('memo_id', flat=True)
233
234            data['responder']['completed'] = MemoSerializer(
235                MemoSnapshot.objects.filter(
236                    memo_id__in=completed_memo_ids
237                ).order_by("-timestamp"),
238                many=True
239            ).data
240        except Exception:
241            data['responder']['completed'] = []
242
243
244        try:
245            active_work_memo_ids = MemoEvents.objects.filter(
246                event_type__in=['attended', 'completed', 'incomplete'],
247                event_by=user
248            ).values_list('memo_id', flat=True)
249
250            tagged_memo_ids = MemoEvents.objects.filter(
251                event_type='tagged',
252                payload__tagged_role_id=str(user.role.id)
253            ).exclude(
254                memo_id__in=active_work_memo_ids
255            ).values_list('memo_id', flat=True)
256
257            data['responder']['Tagged Memos'] = MemoSerializer(
258                MemoSnapshot.objects.filter(
259                    memo_id__in=tagged_memo_ids
260                ).order_by("-timestamp"),
261                many=True
262            ).data
263        except Exception:
264            data['responder']['Tagged Memos'] = []
265
266        return data
267
268    def get_admin_memos(self, user):
269        """
270        Return memos for admins:
271        - 'Pending Approval': Memos not completed and waiting for approval at any level.
272        - 'Completed': Memos approved at all levels and completed.
273        """
274        data = {'admin': {}}
275        data['admin']['all'] = MemoSerializer(MemoSnapshot.objects.all(),many=True).data
276        try:
277            # Memos not completed and waiting for approval (assuming approval_history tracks all levels)
278            pending_approval = MemoSnapshot.objects.filter(
279                memo__hospital=user.hospital,
280                info__status="ongoing",  # adjust as per your status values
281                memo__is_deleted=False
282            ).order_by("-timestamp")
283
284            data['admin']['Pending Approval'] = MemoSerializer(
285                pending_approval, many=True
286            ).data
287        except Exception:
288            data['admin']['Pending Approval'] = []
289
290        try:
291            # Memos completed and approved at all levels
292            completed = MemoSnapshot.objects.filter(
293                memo__hospital=user.hospital,
294                info__status="completed",
295                memo__is_deleted=False
296            ).order_by("-timestamp")
297
298            data['admin']['Completed'] = MemoSerializer(
299                completed, many=True
300            ).data
301        except Exception:
302            data['admin']['Completed'] = []
303        try:
304            # Memos completed and approved at all levels
305            deleted = MemoSnapshot.objects.filter(
306                memo__hospital=user.hospital,
307                memo__is_deleted=True,
308            ).order_by("-timestamp")
309
310            data['admin']['Deleted Memos'] = MemoSerializer(
311                deleted, many=True
312            ).data
313        except Exception:
314            data['admin']['Deleted Memos'] = []
315
316        return data
317
318    def list(self, request, *args, **kwargs):
319        user = request.user
320        role = user.role
321
322        if user.is_superuser or user.is_staff:
323            data = self.get_admin_memos(user)
324        elif role.is_creator:
325            data = self.get_creator_memos(user)
326        elif role.is_approver:
327            data = self.get_approver_memos(user)
328        elif role.is_responder:
329            data = self.get_responder_memos(user)
330        else:
331            data = {"detail": "Unsupported role"}
332
333        return Response(data)
334
335    def get(self, request, *args, **kwargs):
336        return self.list(request, *args, **kwargs)

Return structured memo categories depending on user role. Categories: creator, approver, responder, admin.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
serializer_class = <class 'app.memos.serializers.MemoSerializer'>
def get_serializer_context(self):
87    def get_serializer_context(self):
88        context = super().get_serializer_context()
89        context["hospital_id"] = self.kwargs["hospital_id"]
90        return context

Extra context provided to the serializer class.

def get_creator_memos(self, user):
 92    def get_creator_memos(self, user):
 93        data = {'creator': {}}
 94        try:
 95            data['creator']['My Memos'] = MemoSerializer(
 96                MemoSnapshot.objects.filter(
 97                    info__user_id=user.id, info__status="ongoing"
 98                ).order_by("-timestamp"), many=True
 99            ).data
100        except Exception:
101            data['creator']['My Memos'] = []
102
103        try:
104            data['creator']['Local Memos'] = MemoSerializer(
105                MemoSnapshot.objects.filter(
106                    info__current_ward_id=user.current_ward.id
107                ).order_by("-timestamp"), many=True
108            ).data
109        except Exception:
110            data['creator']['Local Memos'] = []
111
112        try:
113            data['creator']['Completed'] = MemoSerializer(
114                MemoSnapshot.objects.filter(
115                    info__user_id=user.id, info__status="completed"
116                ).order_by("-timestamp"), many=True
117            ).data
118        except Exception:
119            data['creator']['Completed'] = []
120
121        return data
def get_approver_memos(self, user):
123    def get_approver_memos(self, user):
124        role_id = user.role.id
125        key = f"{role_id}_approved"
126        data = {'approver': {}}
127
128        try:
129            waiting_approval = MemoSnapshot.objects.filter(
130                **{f"approval_history__{key}": False}
131            ).order_by("-timestamp")
132
133            data['approver']['Under Review'] = MemoSerializer(
134                waiting_approval, many=True
135            ).data
136        except Exception:
137            data['approver']['Under Review'] = []
138
139        try:
140            user_id = user.id
141
142            latest_approval_time = MemoEvents.objects.filter(
143                event_type='approved',
144                event_by_id=user_id,
145                memo_id=OuterRef('memo_id')
146            ).order_by('-event_timestamp').values('event_timestamp')[:1]
147
148            rejected_after = MemoEvents.objects.filter(
149                event_type='rejected',
150                event_by_id=user_id,
151                memo_id=OuterRef('memo_id'),
152                event_timestamp__gt=Subquery(latest_approval_time)
153            )
154
155            memos_user_approved_and_not_rejected = MemoEvents.objects.filter(
156                event_type='approved',
157                event_by_id=user_id
158            ).annotate(
159                rejected_after=Exists(rejected_after)
160            ).filter(
161                Q(rejected_after=False)
162            ).values_list('memo_id', flat=True).distinct()
163
164            previous_blocks = MemoSnapshot.objects.filter(
165                memo_id__in=memos_user_approved_and_not_rejected
166            ).order_by("-timestamp")
167
168            completed = previous_blocks.filter(
169                info__status="completed"
170            ).order_by("-timestamp")
171
172            previous_blocks = previous_blocks.exclude(
173                memo_id__in=completed.values_list('memo_id', flat=True)
174            ).order_by("-timestamp")
175
176            data['approver']['Reviewed'] = MemoSerializer(
177                previous_blocks, many=True
178            ).data
179            data['approver']['completed'] = MemoSerializer(
180                completed, many=True
181            ).data
182        except Exception:
183            data['approver']['Reviewed'] = []
184            data['approver']['completed'] = []
185
186        return data
def get_responder_memos(self, user):
188    def get_responder_memos(self, user):
189        data = {'responder': {}}
190
191        try:
192            data['responder']['Open Memos'] = MemoSerializer(
193                MemoSnapshot.objects.filter(
194                    worker_status=[],
195                    info__tagged_role_id=str(user.role.id)
196                ).order_by("-timestamp"),
197                many=True
198            ).data
199        except Exception:
200            data['responder']['Open Memos'] = []
201            
202        try:
203            latest_attended = MemoEvents.objects.filter(
204                event_type='attended', event_by=user
205            )
206
207            completed_after = MemoEvents.objects.filter(
208                event_type='completed',
209                event_by=user,
210                memo_id=OuterRef('memo_id'),
211                event_timestamp__gt=OuterRef('event_timestamp')
212            )
213
214            attending_memo_ids = latest_attended.annotate(
215                has_completed=Exists(completed_after)
216            ).filter(
217                has_completed=False
218            ).values_list('memo_id', flat=True)
219
220            data['responder']['Attending'] = MemoSerializer(
221                MemoSnapshot.objects.filter(
222                    memo_id__in=attending_memo_ids
223                ).order_by("-timestamp"),
224                many=True
225            ).data
226        except Exception:
227            data['responder']['Attending'] = []
228
229        try:
230            completed_memo_ids = MemoEvents.objects.filter(
231                event_type='completed', event_by=user
232            ).values_list('memo_id', flat=True)
233
234            data['responder']['completed'] = MemoSerializer(
235                MemoSnapshot.objects.filter(
236                    memo_id__in=completed_memo_ids
237                ).order_by("-timestamp"),
238                many=True
239            ).data
240        except Exception:
241            data['responder']['completed'] = []
242
243
244        try:
245            active_work_memo_ids = MemoEvents.objects.filter(
246                event_type__in=['attended', 'completed', 'incomplete'],
247                event_by=user
248            ).values_list('memo_id', flat=True)
249
250            tagged_memo_ids = MemoEvents.objects.filter(
251                event_type='tagged',
252                payload__tagged_role_id=str(user.role.id)
253            ).exclude(
254                memo_id__in=active_work_memo_ids
255            ).values_list('memo_id', flat=True)
256
257            data['responder']['Tagged Memos'] = MemoSerializer(
258                MemoSnapshot.objects.filter(
259                    memo_id__in=tagged_memo_ids
260                ).order_by("-timestamp"),
261                many=True
262            ).data
263        except Exception:
264            data['responder']['Tagged Memos'] = []
265
266        return data
def get_admin_memos(self, user):
268    def get_admin_memos(self, user):
269        """
270        Return memos for admins:
271        - 'Pending Approval': Memos not completed and waiting for approval at any level.
272        - 'Completed': Memos approved at all levels and completed.
273        """
274        data = {'admin': {}}
275        data['admin']['all'] = MemoSerializer(MemoSnapshot.objects.all(),many=True).data
276        try:
277            # Memos not completed and waiting for approval (assuming approval_history tracks all levels)
278            pending_approval = MemoSnapshot.objects.filter(
279                memo__hospital=user.hospital,
280                info__status="ongoing",  # adjust as per your status values
281                memo__is_deleted=False
282            ).order_by("-timestamp")
283
284            data['admin']['Pending Approval'] = MemoSerializer(
285                pending_approval, many=True
286            ).data
287        except Exception:
288            data['admin']['Pending Approval'] = []
289
290        try:
291            # Memos completed and approved at all levels
292            completed = MemoSnapshot.objects.filter(
293                memo__hospital=user.hospital,
294                info__status="completed",
295                memo__is_deleted=False
296            ).order_by("-timestamp")
297
298            data['admin']['Completed'] = MemoSerializer(
299                completed, many=True
300            ).data
301        except Exception:
302            data['admin']['Completed'] = []
303        try:
304            # Memos completed and approved at all levels
305            deleted = MemoSnapshot.objects.filter(
306                memo__hospital=user.hospital,
307                memo__is_deleted=True,
308            ).order_by("-timestamp")
309
310            data['admin']['Deleted Memos'] = MemoSerializer(
311                deleted, many=True
312            ).data
313        except Exception:
314            data['admin']['Deleted Memos'] = []
315
316        return data

Return memos for admins:

  • 'Pending Approval': Memos not completed and waiting for approval at any level.
  • 'Completed': Memos approved at all levels and completed.
def list(self, request, *args, **kwargs):
318    def list(self, request, *args, **kwargs):
319        user = request.user
320        role = user.role
321
322        if user.is_superuser or user.is_staff:
323            data = self.get_admin_memos(user)
324        elif role.is_creator:
325            data = self.get_creator_memos(user)
326        elif role.is_approver:
327            data = self.get_approver_memos(user)
328        elif role.is_responder:
329            data = self.get_responder_memos(user)
330        else:
331            data = {"detail": "Unsupported role"}
332
333        return Response(data)
def get(self, request, *args, **kwargs):
335    def get(self, request, *args, **kwargs):
336        return self.list(request, *args, **kwargs)
schema
@extend_schema(summary='Retrieve or update memo snapshot', description='GET: Retrieve latest snapshot for a memo. PATCH: Log update event and return new snapshot.', responses={200: MemoSnapshotSerializer})
class MemoDetailView(rest_framework.generics.RetrieveAPIView):
340@extend_schema(
341    summary="Retrieve or update memo snapshot",
342    description="GET: Retrieve latest snapshot for a memo. PATCH: Log update event and return new snapshot.",
343    responses={200: MemoSnapshotSerializer}
344)
345class MemoDetailView(generics.RetrieveAPIView):
346    """Retrieve latest snapshot or update memo via event logging."""
347    serializer_class = MemoSnapshotSerializer
348    lookup_field = "memoId"
349    permission_classes = [permissions.IsAuthenticated]
350
351    def get_object(self):
352        """Return the latest snapshot for the requested memo."""
353        hospital_id = self.kwargs["hospital_id"]
354        memo_id = self.kwargs["memoId"]
355        memo = get_object_or_404(
356            Memo,
357            # hospital_id=hospital_id,
358            memoId=memo_id,
359            is_deleted=False,
360        )
361        snapshot = memo.snapshots.order_by("-timestamp").first()
362        if not snapshot:
363            raise Http404("No snapshots found for this memo.")
364        return snapshot
365
366    def patch(self, request, *args, **kwargs):
367        hospital = get_object_or_404(Hospital, pk=kwargs["hospital_id"])
368        memo = get_object_or_404(
369            Memo,
370            hospital=hospital,
371            memoId=kwargs["memoId"],
372            is_deleted=False,
373        )
374
375        # Log the update event; post_save signal will create a new snapshot
376        MemoEvents.objects.create(
377            memo=memo,
378            event_type="updated",
379            event_by=request.user,
380            payload=request.data.get("payload", {}),
381            metadata=request.data.get("metadata", {}),
382        )
383
384        # Fetch and return the newly created snapshot
385        snapshot = memo.snapshots.order_by("-timestamp").first()
386        return Response(
387            MemoSnapshotSerializer(snapshot).data,
388            status=status.HTTP_200_OK,
389        )

Retrieve latest snapshot or update memo via event logging.

serializer_class = <class 'app.memos.serializers.MemoSnapshotSerializer'>
lookup_field = 'memoId'
permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def get_object(self):
351    def get_object(self):
352        """Return the latest snapshot for the requested memo."""
353        hospital_id = self.kwargs["hospital_id"]
354        memo_id = self.kwargs["memoId"]
355        memo = get_object_or_404(
356            Memo,
357            # hospital_id=hospital_id,
358            memoId=memo_id,
359            is_deleted=False,
360        )
361        snapshot = memo.snapshots.order_by("-timestamp").first()
362        if not snapshot:
363            raise Http404("No snapshots found for this memo.")
364        return snapshot

Return the latest snapshot for the requested memo.

def patch(self, request, *args, **kwargs):
366    def patch(self, request, *args, **kwargs):
367        hospital = get_object_or_404(Hospital, pk=kwargs["hospital_id"])
368        memo = get_object_or_404(
369            Memo,
370            hospital=hospital,
371            memoId=kwargs["memoId"],
372            is_deleted=False,
373        )
374
375        # Log the update event; post_save signal will create a new snapshot
376        MemoEvents.objects.create(
377            memo=memo,
378            event_type="updated",
379            event_by=request.user,
380            payload=request.data.get("payload", {}),
381            metadata=request.data.get("metadata", {}),
382        )
383
384        # Fetch and return the newly created snapshot
385        snapshot = memo.snapshots.order_by("-timestamp").first()
386        return Response(
387            MemoSnapshotSerializer(snapshot).data,
388            status=status.HTTP_200_OK,
389        )
schema
@extend_schema(summary='Approve a memo', description='Approve a memo and log the event.', request=None, responses={200: OpenApiResponse(description='Memo approved successfully.')})
class MemoApproveView(rest_framework.views.APIView):
392@extend_schema(
393    summary="Approve a memo",
394    description="Approve a memo and log the event.",
395    request=None,
396    responses={200: OpenApiResponse(description="Memo approved successfully.")}
397)
398class MemoApproveView(APIView):
399    """Approve a memo and log the event."""
400
401    permission_classes = [permissions.IsAuthenticated]
402
403    def post(self, request, hospital_id, memoId):
404        hospital = get_object_or_404(Hospital, pk=hospital_id)
405        memo = get_object_or_404(Memo,
406            hospital=hospital,
407            memoId=memoId,
408            is_deleted=False,
409        )
410
411        # Log the approval event
412        MemoEvents.objects.create(
413            memo=memo,
414            event_type="approved",
415            event_by=request.user,
416            payload=request.data.get("payload", {}),
417            metadata=request.data.get("metadata", {}),
418        )
419
420        return Response(
421            {"message": "Memo approved successf ully."},
422            status=status.HTTP_200_OK,
423        )

Approve a memo and log the event.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def post(self, request, hospital_id, memoId):
403    def post(self, request, hospital_id, memoId):
404        hospital = get_object_or_404(Hospital, pk=hospital_id)
405        memo = get_object_or_404(Memo,
406            hospital=hospital,
407            memoId=memoId,
408            is_deleted=False,
409        )
410
411        # Log the approval event
412        MemoEvents.objects.create(
413            memo=memo,
414            event_type="approved",
415            event_by=request.user,
416            payload=request.data.get("payload", {}),
417            metadata=request.data.get("metadata", {}),
418        )
419
420        return Response(
421            {"message": "Memo approved successf ully."},
422            status=status.HTTP_200_OK,
423        )
schema
@extend_schema(summary='Reject a memo', description='Reject a memo and log the event.', request=None, responses={200: OpenApiResponse(description='Memo rejected successfully.')})
class MemoRejectView(rest_framework.views.APIView):
425@extend_schema(
426    summary="Reject a memo",
427    description="Reject a memo and log the event.",
428    request=None,
429    responses={200: OpenApiResponse(description="Memo rejected successfully.")}
430)
431class MemoRejectView(APIView):
432    """Reject a memo and log the event."""
433
434    permission_classes = [permissions.IsAuthenticated]
435
436    def post(self, request, hospital_id, memoId):
437        hospital = get_object_or_404(Hospital, pk=hospital_id)
438        memo = get_object_or_404(Memo,
439            hospital=hospital,
440            memoId=memoId,
441            is_deleted=False,
442        )
443
444        # Log the approval event
445        MemoEvents.objects.create(
446            memo=memo,
447            event_type="rejected",
448            event_by=request.user,
449            payload=request.data.get("payload", {}),
450            metadata=request.data.get("metadata", {}),
451        )
452
453        return Response(
454            {"message": "Memo rejected successfully."},
455            status=status.HTTP_200_OK,
456        )

Reject a memo and log the event.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def post(self, request, hospital_id, memoId):
436    def post(self, request, hospital_id, memoId):
437        hospital = get_object_or_404(Hospital, pk=hospital_id)
438        memo = get_object_or_404(Memo,
439            hospital=hospital,
440            memoId=memoId,
441            is_deleted=False,
442        )
443
444        # Log the approval event
445        MemoEvents.objects.create(
446            memo=memo,
447            event_type="rejected",
448            event_by=request.user,
449            payload=request.data.get("payload", {}),
450            metadata=request.data.get("metadata", {}),
451        )
452
453        return Response(
454            {"message": "Memo rejected successfully."},
455            status=status.HTTP_200_OK,
456        )
schema
@extend_schema(summary='Create, update, or list attendee ETA for a memo', description='GET: List all attendee ETAs for a memo. POST: Create ETA. PUT: Update ETA for current user.', request=AttendeeETASerializer, responses={200: AttendeeETASerializer(many=True), 201: AttendeeETASerializer})
class AttendeeETACreateView(rest_framework.views.APIView):
475@extend_schema(
476    summary="Create, update, or list attendee ETA for a memo",
477    description="GET: List all attendee ETAs for a memo. POST: Create ETA. PUT: Update ETA for current user.",
478    request=AttendeeETASerializer,
479    responses={200: AttendeeETASerializer(many=True), 201: AttendeeETASerializer}
480)
481class AttendeeETACreateView(APIView):
482    """Manage attendee ETA for a memo."""
483    permission_classes = [IsAuthenticated]
484
485    def get(self, request, hospital_id, memoId):
486        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
487        # Return list of all attendee ETAs for this memo
488        etas = AttendeeETA.objects.filter(memo=memo)
489        serializer = AttendeeETASerializer(etas, many=True)
490        return Response(serializer.data, status=status.HTTP_200_OK)
491
492    def post(self, request, hospital_id, memoId):
493        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
494        data = request.data.copy()
495        data['memo'] = memo.memoId
496        data['attendee'] = request.user.id
497        serializer = AttendeeETASerializer(data=data)
498        if serializer.is_valid():
499            serializer.save()
500            return Response(serializer.data, status=status.HTTP_201_CREATED)
501        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
502
503    def put(self, request, hospital_id, memoId):
504        """Update current user's ETA for this memo"""
505        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
506        try:
507            attendee_eta = AttendeeETA.objects.get(memo=memo, attendee=request.user)
508        except AttendeeETA.DoesNotExist:
509            return Response({"detail": "ETA not found for current user."}, status=status.HTTP_404_NOT_FOUND)
510
511        serializer = AttendeeETASerializer(attendee_eta, data=request.data, partial=True)
512        if serializer.is_valid():
513            serializer.save()
514            return Response(serializer.data, status=status.HTTP_200_OK)
515        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Manage attendee ETA for a memo.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def get(self, request, hospital_id, memoId):
485    def get(self, request, hospital_id, memoId):
486        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
487        # Return list of all attendee ETAs for this memo
488        etas = AttendeeETA.objects.filter(memo=memo)
489        serializer = AttendeeETASerializer(etas, many=True)
490        return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, hospital_id, memoId):
492    def post(self, request, hospital_id, memoId):
493        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
494        data = request.data.copy()
495        data['memo'] = memo.memoId
496        data['attendee'] = request.user.id
497        serializer = AttendeeETASerializer(data=data)
498        if serializer.is_valid():
499            serializer.save()
500            return Response(serializer.data, status=status.HTTP_201_CREATED)
501        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def put(self, request, hospital_id, memoId):
503    def put(self, request, hospital_id, memoId):
504        """Update current user's ETA for this memo"""
505        memo = get_object_or_404(Memo, hospital_id=hospital_id, memoId=memoId, is_deleted=False)
506        try:
507            attendee_eta = AttendeeETA.objects.get(memo=memo, attendee=request.user)
508        except AttendeeETA.DoesNotExist:
509            return Response({"detail": "ETA not found for current user."}, status=status.HTTP_404_NOT_FOUND)
510
511        serializer = AttendeeETASerializer(attendee_eta, data=request.data, partial=True)
512        if serializer.is_valid():
513            serializer.save()
514            return Response(serializer.data, status=status.HTTP_200_OK)
515        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Update current user's ETA for this memo

schema
@extend_schema(summary='Update memo work status', description='POST: Update memo work status (attended, completed, etc). DELETE: Delete work status.', request=None, responses={200: OpenApiResponse(description='Memo work status updated/deleted successfully.')})
class MemoWorkStatusView(rest_framework.views.APIView):
518@extend_schema(
519    summary="Update memo work status",
520    description="POST: Update memo work status (attended, completed, etc). DELETE: Delete work status.",
521    request=None,
522    responses={200: OpenApiResponse(description="Memo work status updated/deleted successfully.")}
523)
524class MemoWorkStatusView(APIView):
525    """Update or delete memo work status."""
526
527    permission_classes = [permissions.IsAuthenticated]
528
529    def post(self, request, hospital_id, memoId):
530        hospital = get_object_or_404(Hospital, pk=hospital_id)
531        memo = get_object_or_404(Memo,
532            hospital=hospital,
533            memoId=memoId,
534            is_deleted=False,
535        )
536        
537        payload = request.data.get('payload')
538        otp =  payload.get("otp")
539        if otp and request.data.get('event_type') in ['attended','incomplete','tagged']:
540            if memo.latest_snapshot().attendee_otp == otp:
541                pass
542            else:
543                return Response({"isValid":False},status.HTTP_202_ACCEPTED) 
544        
545        if otp and request.data.get('event_type') == "completed":
546            if memo.latest_snapshot().completion_otp == otp:
547                pass
548            else:
549                print("otp mismatch")
550                return Response({"isValid":False},status.HTTP_202_ACCEPTED) 
551
552        # Log the approval event
553        MemoEvents.objects.create(
554            memo=memo,
555            event_type=request.data.get("event_type", "attended"),
556            event_by=request.user,
557            payload=request.data.get("payload", {}),
558            metadata=request.data.get("metadata", {}),
559        )
560
561        return Response(
562            {"message": "Memo work status updated successfully.","isValid":True},
563            status=status.HTTP_200_OK,
564        )
565        
566    def delete(self, request, hospital_id, memoId):
567        hospital = get_object_or_404(Hospital, pk=hospital_id)
568        memo = get_object_or_404(Memo,
569            hospital=hospital,
570            memoId=memoId,
571            is_deleted=False,
572        )
573
574        # Log the approval event
575        MemoEvents.objects.create(
576            memo=memo,
577            event_type="worker_status_delete",
578            event_by=request.user,
579            payload=request.data.get("payload", {}),
580            metadata=request.data.get("metadata", {}),
581        )
582
583        return Response(
584            {"message": "Memo work status deleted successfully."},
585            status=status.HTTP_200_OK,
586        )

Update or delete memo work status.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def post(self, request, hospital_id, memoId):
529    def post(self, request, hospital_id, memoId):
530        hospital = get_object_or_404(Hospital, pk=hospital_id)
531        memo = get_object_or_404(Memo,
532            hospital=hospital,
533            memoId=memoId,
534            is_deleted=False,
535        )
536        
537        payload = request.data.get('payload')
538        otp =  payload.get("otp")
539        if otp and request.data.get('event_type') in ['attended','incomplete','tagged']:
540            if memo.latest_snapshot().attendee_otp == otp:
541                pass
542            else:
543                return Response({"isValid":False},status.HTTP_202_ACCEPTED) 
544        
545        if otp and request.data.get('event_type') == "completed":
546            if memo.latest_snapshot().completion_otp == otp:
547                pass
548            else:
549                print("otp mismatch")
550                return Response({"isValid":False},status.HTTP_202_ACCEPTED) 
551
552        # Log the approval event
553        MemoEvents.objects.create(
554            memo=memo,
555            event_type=request.data.get("event_type", "attended"),
556            event_by=request.user,
557            payload=request.data.get("payload", {}),
558            metadata=request.data.get("metadata", {}),
559        )
560
561        return Response(
562            {"message": "Memo work status updated successfully.","isValid":True},
563            status=status.HTTP_200_OK,
564        )
def delete(self, request, hospital_id, memoId):
566    def delete(self, request, hospital_id, memoId):
567        hospital = get_object_or_404(Hospital, pk=hospital_id)
568        memo = get_object_or_404(Memo,
569            hospital=hospital,
570            memoId=memoId,
571            is_deleted=False,
572        )
573
574        # Log the approval event
575        MemoEvents.objects.create(
576            memo=memo,
577            event_type="worker_status_delete",
578            event_by=request.user,
579            payload=request.data.get("payload", {}),
580            metadata=request.data.get("metadata", {}),
581        )
582
583        return Response(
584            {"message": "Memo work status deleted successfully."},
585            status=status.HTTP_200_OK,
586        )
schema
@extend_schema(summary='Refresh OTP for memo', description='Refresh and return attendee/completion OTP for a memo.', request=None, responses={200: OpenApiResponse(description='OTP refreshed successfully.')})
class RefreshOTPView(rest_framework.views.APIView):
589@extend_schema(
590    summary="Refresh OTP for memo",
591    description="Refresh and return attendee/completion OTP for a memo.",
592    request=None,
593    responses={200: OpenApiResponse(description="OTP refreshed successfully.")}
594)
595class RefreshOTPView(APIView):
596    """Refresh and return attendee/completion OTP for a memo."""
597
598    permission_classes = [permissions.IsAuthenticated]
599
600    def post(self, request, hospital_id, memoId):
601        hospital = get_object_or_404(Hospital, pk=hospital_id)
602        memo = get_object_or_404(Memo,
603            hospital=hospital,
604            memoId=memoId,
605            is_deleted=False,
606        )
607            
608        snapshot = memo.latest_snapshot()
609        if snapshot:
610            attendee_otp = snapshot.generate_otp()
611            # completion_otp = snapshot.generate_otp()
612            
613            snapshot.attendee_otp = attendee_otp
614            # snapshot.completion_otp = completion_otp
615            
616            snapshot.save()
617            
618            return Response({
619                "attendee_otp":attendee_otp,
620                "completion_otp":snapshot.completion_otp
621            },status=status.HTTP_200_OK)
622            
623        else:
624            return Response({"message":"some error"},status=status.HTTP_404_NOT_FOUND)

Refresh and return attendee/completion OTP for a memo.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
def post(self, request, hospital_id, memoId):
600    def post(self, request, hospital_id, memoId):
601        hospital = get_object_or_404(Hospital, pk=hospital_id)
602        memo = get_object_or_404(Memo,
603            hospital=hospital,
604            memoId=memoId,
605            is_deleted=False,
606        )
607            
608        snapshot = memo.latest_snapshot()
609        if snapshot:
610            attendee_otp = snapshot.generate_otp()
611            # completion_otp = snapshot.generate_otp()
612            
613            snapshot.attendee_otp = attendee_otp
614            # snapshot.completion_otp = completion_otp
615            
616            snapshot.save()
617            
618            return Response({
619                "attendee_otp":attendee_otp,
620                "completion_otp":snapshot.completion_otp
621            },status=status.HTTP_200_OK)
622            
623        else:
624            return Response({"message":"some error"},status=status.HTTP_404_NOT_FOUND)
schema
@extend_schema(summary='Dashboard memos with date filtering', description='Return structured memo categories depending on user role with date filtering (date, week, month).', parameters=[OpenApiParameter(name='period_type', type=str, location='query', required=False, description='Type of period filter: date, week, month'), OpenApiParameter(name='value', type=str, location='query', required=False, description='Date value (YYYY-MM-DD)'), OpenApiParameter(name='year', type=int, location='query', required=False, description='Year for week/month filter'), OpenApiParameter(name='week', type=int, location='query', required=False, description='Week number for week filter'), OpenApiParameter(name='month', type=int, location='query', required=False, description='Month number for month filter')], responses={200: MemoSnapshotSerializer(many=True)})
class DashboardMemosView(rest_framework.generics.GenericAPIView):
628@extend_schema(
629    summary="Dashboard memos with date filtering",
630    description="Return structured memo categories depending on user role with date filtering (date, week, month).",
631    parameters=[
632        OpenApiParameter(name="period_type", type=str, location="query", required=False, description="Type of period filter: date, week, month"),
633        OpenApiParameter(name="value", type=str, location="query", required=False, description="Date value (YYYY-MM-DD)"),
634        OpenApiParameter(name="year", type=int, location="query", required=False, description="Year for week/month filter"),
635        OpenApiParameter(name="week", type=int, location="query", required=False, description="Week number for week filter"),
636        OpenApiParameter(name="month", type=int, location="query", required=False, description="Month number for month filter"),
637    ],
638    responses={200: MemoSnapshotSerializer(many=True)}
639)
640class DashboardMemosView(generics.GenericAPIView):
641    """
642    Return structured memo categories depending on user role with date filtering.
643    Supports filtering by date, week, or month based on memo creation time.
644    """
645    permission_classes = [permissions.IsAuthenticated]
646    serializer_class = MemoSnapshotSerializer
647
648    def get_serializer_context(self):
649        context = super().get_serializer_context()
650        context["hospital_id"] = self.kwargs["hospital_id"]
651        return context
652    
653    def get_date_filter(self, request):
654        """
655        Parse date filtering parameters from request and return Q object for filtering.
656        Expected parameters:
657        - period_type: 'date', 'week', or 'month'
658        - For 'date': value (YYYY-MM-DD format)
659        - For 'week': year, week
660        - For 'month': year, month
661        """
662        period_type = request.query_params.get('period_type', 'month')
663        
664        if period_type == 'date':
665            # Filter by specific date
666            date_value = request.query_params.get('value')
667            if date_value:
668                try:
669                    target_date = datetime.strptime(date_value, '%Y-%m-%d').date()
670                    start_datetime = timezone.make_aware(
671                        datetime.combine(target_date, datetime.min.time())
672                    )
673                    end_datetime = start_datetime + timedelta(days=1)
674                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
675                except ValueError:
676                    # Invalid date format, return no filter
677                    pass
678        
679        elif period_type == 'week':
680            # Filter by specific week
681            year = request.query_params.get('year')
682            week = request.query_params.get('week')
683            if year and week:
684                try:
685                    year = int(year)
686                    week = int(week)
687                    
688                    # Calculate the start of the week (Monday)
689                    # ISO week date calculation
690                    jan_1 = datetime(year, 1, 1)
691                    week_start = jan_1 + timedelta(days=(week - 1) * 7 - jan_1.weekday())
692                    week_end = week_start + timedelta(days=7)
693                    
694                    start_datetime = timezone.make_aware(week_start)
695                    end_datetime = timezone.make_aware(week_end)
696                    
697                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
698                except (ValueError, TypeError):
699                    # Invalid year/week format, return no filter
700                    pass
701        
702        elif period_type == 'month':
703            # Filter by specific month
704            year = request.query_params.get('year')
705            month = request.query_params.get('month')
706            if year and month:
707                try:
708                    year = int(year)
709                    month = int(month)
710                    
711                    # Get first day of the month
712                    start_date = datetime(year, month, 1)
713                    # Get first day of next month (or next year if December)
714                    if month == 12:
715                        end_date = datetime(year + 1, 1, 1)
716                    else:
717                        end_date = datetime(year, month + 1, 1)
718                    
719                    start_datetime = timezone.make_aware(start_date)
720                    end_datetime = timezone.make_aware(end_date)
721                    
722                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
723                except (ValueError, TypeError):
724                    # Invalid year/month format, return no filter
725                    pass
726        
727        # Default: return all records (no date filter)
728        return Q()
729    
730    def get_queryset(self):
731        """
732        Get filtered queryset based on hospital and date parameters.
733        """
734        h_id = self.kwargs["hospital_id"]
735        date_filter = self.get_date_filter(self.request)
736        
737        # Get memos for the hospital with date filtering
738        memos = Memo.objects.filter(
739            hospital__id=h_id,
740            is_deleted=False  # Exclude deleted memos
741        ).filter(date_filter)
742        
743        return memos
744    
745    def get(self, request, *args, **kwargs):
746        """
747        Return memos with their latest snapshots for the specified hospital and date range.
748        """
749        try:
750            memos = self.get_queryset()
751            memos_snapshots = [memo.latest_snapshot() for memo in memos]
752            # Build response data with memo and latest snapshot info
753            memo_data = MemoSerializer(memos_snapshots,many=True).data
754            return Response({
755                'count': len(memo_data),
756                'memos': memo_data,
757                'filters': {
758                    'period_type': request.query_params.get('period_type', 'month'),
759                    'hospital_id': kwargs["hospital_id"]
760                }
761            }, status=status.HTTP_200_OK)
762            
763        except Exception as e:
764            return Response({
765                'error': 'Failed to fetch memos',
766                'detail': str(e)
767            }, status=status.HTTP_400_BAD_REQUEST)

Return structured memo categories depending on user role with date filtering. Supports filtering by date, week, or month based on memo creation time.

permission_classes = [<class 'rest_framework.permissions.IsAuthenticated'>]
serializer_class = <class 'app.memos.serializers.MemoSnapshotSerializer'>
def get_serializer_context(self):
648    def get_serializer_context(self):
649        context = super().get_serializer_context()
650        context["hospital_id"] = self.kwargs["hospital_id"]
651        return context

Extra context provided to the serializer class.

def get_date_filter(self, request):
653    def get_date_filter(self, request):
654        """
655        Parse date filtering parameters from request and return Q object for filtering.
656        Expected parameters:
657        - period_type: 'date', 'week', or 'month'
658        - For 'date': value (YYYY-MM-DD format)
659        - For 'week': year, week
660        - For 'month': year, month
661        """
662        period_type = request.query_params.get('period_type', 'month')
663        
664        if period_type == 'date':
665            # Filter by specific date
666            date_value = request.query_params.get('value')
667            if date_value:
668                try:
669                    target_date = datetime.strptime(date_value, '%Y-%m-%d').date()
670                    start_datetime = timezone.make_aware(
671                        datetime.combine(target_date, datetime.min.time())
672                    )
673                    end_datetime = start_datetime + timedelta(days=1)
674                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
675                except ValueError:
676                    # Invalid date format, return no filter
677                    pass
678        
679        elif period_type == 'week':
680            # Filter by specific week
681            year = request.query_params.get('year')
682            week = request.query_params.get('week')
683            if year and week:
684                try:
685                    year = int(year)
686                    week = int(week)
687                    
688                    # Calculate the start of the week (Monday)
689                    # ISO week date calculation
690                    jan_1 = datetime(year, 1, 1)
691                    week_start = jan_1 + timedelta(days=(week - 1) * 7 - jan_1.weekday())
692                    week_end = week_start + timedelta(days=7)
693                    
694                    start_datetime = timezone.make_aware(week_start)
695                    end_datetime = timezone.make_aware(week_end)
696                    
697                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
698                except (ValueError, TypeError):
699                    # Invalid year/week format, return no filter
700                    pass
701        
702        elif period_type == 'month':
703            # Filter by specific month
704            year = request.query_params.get('year')
705            month = request.query_params.get('month')
706            if year and month:
707                try:
708                    year = int(year)
709                    month = int(month)
710                    
711                    # Get first day of the month
712                    start_date = datetime(year, month, 1)
713                    # Get first day of next month (or next year if December)
714                    if month == 12:
715                        end_date = datetime(year + 1, 1, 1)
716                    else:
717                        end_date = datetime(year, month + 1, 1)
718                    
719                    start_datetime = timezone.make_aware(start_date)
720                    end_datetime = timezone.make_aware(end_date)
721                    
722                    return Q(created_at__gte=start_datetime, created_at__lt=end_datetime)
723                except (ValueError, TypeError):
724                    # Invalid year/month format, return no filter
725                    pass
726        
727        # Default: return all records (no date filter)
728        return Q()

Parse date filtering parameters from request and return Q object for filtering. Expected parameters:

  • period_type: 'date', 'week', or 'month'
  • For 'date': value (YYYY-MM-DD format)
  • For 'week': year, week
  • For 'month': year, month
def get_queryset(self):
730    def get_queryset(self):
731        """
732        Get filtered queryset based on hospital and date parameters.
733        """
734        h_id = self.kwargs["hospital_id"]
735        date_filter = self.get_date_filter(self.request)
736        
737        # Get memos for the hospital with date filtering
738        memos = Memo.objects.filter(
739            hospital__id=h_id,
740            is_deleted=False  # Exclude deleted memos
741        ).filter(date_filter)
742        
743        return memos

Get filtered queryset based on hospital and date parameters.

def get(self, request, *args, **kwargs):
745    def get(self, request, *args, **kwargs):
746        """
747        Return memos with their latest snapshots for the specified hospital and date range.
748        """
749        try:
750            memos = self.get_queryset()
751            memos_snapshots = [memo.latest_snapshot() for memo in memos]
752            # Build response data with memo and latest snapshot info
753            memo_data = MemoSerializer(memos_snapshots,many=True).data
754            return Response({
755                'count': len(memo_data),
756                'memos': memo_data,
757                'filters': {
758                    'period_type': request.query_params.get('period_type', 'month'),
759                    'hospital_id': kwargs["hospital_id"]
760                }
761            }, status=status.HTTP_200_OK)
762            
763        except Exception as e:
764            return Response({
765                'error': 'Failed to fetch memos',
766                'detail': str(e)
767            }, status=status.HTTP_400_BAD_REQUEST)

Return memos with their latest snapshots for the specified hospital and date range.

schema
def format_memo_row(memo):
771def format_memo_row(memo):
772    info = memo.get("info", {})
773    hierarchy = memo.get("hierarchy", [])
774    approval_history = memo.get("approval_history", {})
775    worker_status = memo.get("worker_status", [])
776
777    row = {
778        "Memo Link": f"https://web.memotrack.net/memos/{memo['memo']}",
779        "Complaint": info.get("complaint", ""),
780        "Raised By": f"{info.get('user_role_name', '')} ({info.get('phone_number', '')})",
781        "Ward + Block + Floor": f"{info.get('current_ward_name', '')}, {info.get('current_block_name', '')}, Floor {info.get('current_floor', '')}",
782        "Time": memo.get("created_at", "")
783    }
784
785    # Add Approver details
786    for idx, approver in enumerate(hierarchy, start=1):
787        role_id = approver["role"]
788        approved_key = f"{role_id}_approved"
789        # Fetching approved status and time from the hierarchy list
790        approved = approver.get("approved", False)
791        approved_time = approver.get("approved_at", "")
792
793
794        row[f"Approver_{idx}_Role"] = approver.get("role_name", "")
795        row[f"Approver_{idx}_Approved"] = "Yes" if approved else "No"
796        row[f"Approver_{idx}_Time"] = approved_time if approved else ""
797        row[f"Approver_{idx}_Phone"] = approver.get("by_phone", "")
798        row[f"Approver_{idx}_Name"] = approver.get("approved_by", "")
799
800    # Worker status comments
801    worker_lines = []
802    for status in worker_status:
803        line = f"{status.get('by_institution_id', '')} ({status.get('by_phone', '')}) at {status.get('timestamp', '')}:\n{status.get('comment', '')}"
804        worker_lines.append(line)
805    row["Worker Status Comments"] = "\n\n".join(worker_lines)
806
807    return row
@extend_schema(summary='Export single memo to Excel', description="Export a single memo's latest snapshot to Excel.", parameters=[OpenApiParameter(name='memo_id', type=str, location='path', required=True, description='Memo ID')], responses={200: OpenApiResponse(description='Excel file with memo data')})
class ExportSingleMemoExcelView(rest_framework.views.APIView):
811@extend_schema(
812    summary="Export single memo to Excel",
813    description="Export a single memo's latest snapshot to Excel.",
814    parameters=[
815        OpenApiParameter(name="memo_id", type=str, location="path", required=True, description="Memo ID"),
816    ],
817    responses={200: OpenApiResponse(description="Excel file with memo data")}
818)
819class ExportSingleMemoExcelView(APIView):
820    permission_classes = [permissions.AllowAny]
821
822    def get(self, request, memo_id):
823        try:
824            memo = Memo.objects.get(memoId=memo_id, is_deleted=False)
825            snapshot = MemoSnapshotSerializer(memo.latest_snapshot()).data
826
827            # Format single row
828            row = format_memo_row(snapshot)
829            df = pd.DataFrame([row])
830
831            output = io.BytesIO()
832            with pd.ExcelWriter(output, engine="openpyxl") as writer:
833                df.to_excel(writer, index=False)
834
835            output.seek(0)
836            filename = f"memo_{memo.memoId}_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
837
838            response = StreamingHttpResponse(
839                output,
840                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
841            )
842            response['Content-Disposition'] = f'attachment; filename="{filename}"'
843            return response
844
845        except Memo.DoesNotExist:
846            raise Http404("Memo not found")
847        except Exception as e:
848            return StreamingHttpResponse(
849                content=f"Failed to export: {str(e)}",
850                status=400,
851                content_type='text/plain'
852            )

Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking.

permission_classes = [<class 'rest_framework.permissions.AllowAny'>]
def get(self, request, memo_id):
822    def get(self, request, memo_id):
823        try:
824            memo = Memo.objects.get(memoId=memo_id, is_deleted=False)
825            snapshot = MemoSnapshotSerializer(memo.latest_snapshot()).data
826
827            # Format single row
828            row = format_memo_row(snapshot)
829            df = pd.DataFrame([row])
830
831            output = io.BytesIO()
832            with pd.ExcelWriter(output, engine="openpyxl") as writer:
833                df.to_excel(writer, index=False)
834
835            output.seek(0)
836            filename = f"memo_{memo.memoId}_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
837
838            response = StreamingHttpResponse(
839                output,
840                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
841            )
842            response['Content-Disposition'] = f'attachment; filename="{filename}"'
843            return response
844
845        except Memo.DoesNotExist:
846            raise Http404("Memo not found")
847        except Exception as e:
848            return StreamingHttpResponse(
849                content=f"Failed to export: {str(e)}",
850                status=400,
851                content_type='text/plain'
852            )
schema
@extend_schema(summary='Export memos to Excel', description='Export all memos for a hospital to Excel with optional date filtering.', parameters=[OpenApiParameter(name='hospital_id', type=int, location='path', required=True, description='Hospital ID'), OpenApiParameter(name='period_type', type=str, location='query', required=False, description='Type of period filter: date, week, month'), OpenApiParameter(name='value', type=str, location='query', required=False, description='Date value (YYYY-MM-DD)'), OpenApiParameter(name='year', type=int, location='query', required=False, description='Year for week/month filter'), OpenApiParameter(name='week', type=int, location='query', required=False, description='Week number for week filter'), OpenApiParameter(name='month', type=int, location='query', required=False, description='Month number for month filter')], responses={200: OpenApiResponse(description='Excel file with memos data')})
class ExportMemosExcelView(rest_framework.views.APIView):
854@extend_schema(
855    summary="Export memos to Excel",
856    description="Export all memos for a hospital to Excel with optional date filtering.",
857    parameters=[
858        OpenApiParameter(name="hospital_id", type=int, location="path", required=True, description="Hospital ID"),
859        OpenApiParameter(name="period_type", type=str, location="query", required=False, description="Type of period filter: date, week, month"),
860        OpenApiParameter(name="value", type=str, location="query", required=False, description="Date value (YYYY-MM-DD)"),
861        OpenApiParameter(name="year", type=int, location="query", required=False, description="Year for week/month filter"),
862        OpenApiParameter(name="week", type=int, location="query", required=False, description="Week number for week filter"),
863        OpenApiParameter(name="month", type=int, location="query", required=False, description="Month number for month filter"),
864    ],
865    responses={200: OpenApiResponse(description="Excel file with memos data")}
866)
867class ExportMemosExcelView(APIView):
868    """Export all memos for a hospital to Excel with optional date filtering."""
869    permission_classes = [permissions.AllowAny]
870
871    def get_date_filter(self, request):
872        period_type = request.query_params.get('period_type', 'month')
873
874        if period_type == 'date':
875            date_value = request.query_params.get('value')
876            if date_value:
877                try:
878                    target_date = datetime.strptime(date_value, '%Y-%m-%d').date()
879                    start = timezone.make_aware(datetime.combine(target_date, datetime.min.time()))
880                    end = start + timedelta(days=1)
881                    return Q(created_at__gte=start, created_at__lt=end)
882                except ValueError:
883                    pass
884
885        elif period_type == 'week':
886            year = request.query_params.get('year')
887            week = request.query_params.get('week')
888            if year and week:
889                try:
890                    year, week = int(year), int(week)
891                    jan_4 = datetime(year, 1, 4)
892                    start = jan_4 + timedelta(weeks=week-1, days=-jan_4.weekday())
893                    end = start + timedelta(days=7)
894                    return Q(created_at__gte=timezone.make_aware(start), created_at__lt=timezone.make_aware(end))
895                except (ValueError, TypeError):
896                    pass
897
898        elif period_type == 'month':
899            year = request.query_params.get('year')
900            month = request.query_params.get('month')
901            if year and month:
902                try:
903                    year, month = int(year), int(month)
904                    start = timezone.make_aware(datetime(year, month, 1))
905                    if month == 12:
906                        end = timezone.make_aware(datetime(year + 1, 1, 1))
907                    else:
908                        end = timezone.make_aware(datetime(year, month + 1, 1))
909                    return Q(created_at__gte=start, created_at__lt=end)
910                except (ValueError, TypeError):
911                    pass
912
913        return Q()
914
915    def get(self, request, hospital_id):
916        try:
917            date_filter = self.get_date_filter(request)
918            memos = Memo.objects.filter(hospital__id=hospital_id, is_deleted=False).filter(date_filter)
919            snapshots = [MemoSnapshotSerializer(memo.latest_snapshot()).data for memo in memos]
920
921            rows = [format_memo_row(snapshot) for snapshot in snapshots]
922            df = pd.DataFrame(rows)
923
924            output = io.BytesIO()
925            with pd.ExcelWriter(output, engine="openpyxl") as writer:
926                df.to_excel(writer, index=False)
927
928            output.seek(0)
929            filename = f"memo_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
930
931            response = StreamingHttpResponse(
932                output,
933                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
934            )
935            response['Content-Disposition'] = f'attachment; filename="{filename}"'
936            return response
937
938        except Exception as e:
939            return StreamingHttpResponse(
940                content=f"Failed to export: {str(e)}",
941                status=400,
942                content_type='text/plain'
943            )

Export all memos for a hospital to Excel with optional date filtering.

permission_classes = [<class 'rest_framework.permissions.AllowAny'>]
def get_date_filter(self, request):
871    def get_date_filter(self, request):
872        period_type = request.query_params.get('period_type', 'month')
873
874        if period_type == 'date':
875            date_value = request.query_params.get('value')
876            if date_value:
877                try:
878                    target_date = datetime.strptime(date_value, '%Y-%m-%d').date()
879                    start = timezone.make_aware(datetime.combine(target_date, datetime.min.time()))
880                    end = start + timedelta(days=1)
881                    return Q(created_at__gte=start, created_at__lt=end)
882                except ValueError:
883                    pass
884
885        elif period_type == 'week':
886            year = request.query_params.get('year')
887            week = request.query_params.get('week')
888            if year and week:
889                try:
890                    year, week = int(year), int(week)
891                    jan_4 = datetime(year, 1, 4)
892                    start = jan_4 + timedelta(weeks=week-1, days=-jan_4.weekday())
893                    end = start + timedelta(days=7)
894                    return Q(created_at__gte=timezone.make_aware(start), created_at__lt=timezone.make_aware(end))
895                except (ValueError, TypeError):
896                    pass
897
898        elif period_type == 'month':
899            year = request.query_params.get('year')
900            month = request.query_params.get('month')
901            if year and month:
902                try:
903                    year, month = int(year), int(month)
904                    start = timezone.make_aware(datetime(year, month, 1))
905                    if month == 12:
906                        end = timezone.make_aware(datetime(year + 1, 1, 1))
907                    else:
908                        end = timezone.make_aware(datetime(year, month + 1, 1))
909                    return Q(created_at__gte=start, created_at__lt=end)
910                except (ValueError, TypeError):
911                    pass
912
913        return Q()
def get(self, request, hospital_id):
915    def get(self, request, hospital_id):
916        try:
917            date_filter = self.get_date_filter(request)
918            memos = Memo.objects.filter(hospital__id=hospital_id, is_deleted=False).filter(date_filter)
919            snapshots = [MemoSnapshotSerializer(memo.latest_snapshot()).data for memo in memos]
920
921            rows = [format_memo_row(snapshot) for snapshot in snapshots]
922            df = pd.DataFrame(rows)
923
924            output = io.BytesIO()
925            with pd.ExcelWriter(output, engine="openpyxl") as writer:
926                df.to_excel(writer, index=False)
927
928            output.seek(0)
929            filename = f"memo_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
930
931            response = StreamingHttpResponse(
932                output,
933                content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
934            )
935            response['Content-Disposition'] = f'attachment; filename="{filename}"'
936            return response
937
938        except Exception as e:
939            return StreamingHttpResponse(
940                content=f"Failed to export: {str(e)}",
941                status=400,
942                content_type='text/plain'
943            )
schema