diff --git a/MisplaceAI/process_misplaced_manager/models.py b/MisplaceAI/process_misplaced_manager/models.py index 5ad5b8f214c0fb02275e937fb5820fafb6a8dcdc..0f074a108f8c177aa82c227e1e75a4fa28627123 100644 --- a/MisplaceAI/process_misplaced_manager/models.py +++ b/MisplaceAI/process_misplaced_manager/models.py @@ -2,11 +2,12 @@ from django.db import models from django.contrib.auth.models import User +from django.utils import timezone class UploadedImage(models.Model): image = models.ImageField(upload_to='images/') uploaded_at = models.DateTimeField(auto_now_add=True) - + user = models.ForeignKey(User, on_delete=models.CASCADE) class UserVideoFramePreference(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) @@ -16,4 +17,27 @@ class UploadedVideo(models.Model): video = models.FileField(upload_to='videos/') uploaded_at = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(User, on_delete=models.CASCADE) - user_video_frame_preference = models.ForeignKey(UserVideoFramePreference, on_delete=models.CASCADE, null=True, blank=True) \ No newline at end of file + user_video_frame_preference = models.ForeignKey(UserVideoFramePreference, on_delete=models.CASCADE, null=True, blank=True) + +class DailyDetectionLimit(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + image_detection_count = models.IntegerField(default=0) + video_detection_count = models.IntegerField(default=0) + last_reset = models.DateField(auto_now_add=True) + + def reset_limits(self): + if self.last_reset < timezone.now().date(): + self.image_detection_count = 0 + self.video_detection_count = 0 + self.last_reset = timezone.now().date() + self.save() + + def __str__(self): + return f'{self.user.username} - Images: {self.image_detection_count}, Videos: {self.video_detection_count}' + +class DetectionLimitSetting(models.Model): + daily_image_limit = models.IntegerField(default=10) # Default limit for images + daily_video_limit = models.IntegerField(default=5) # Default limit for videos + + def __str__(self): + return f'Daily Limits - Images: {self.daily_image_limit}, Videos: {self.daily_video_limit}' diff --git a/MisplaceAI/process_misplaced_manager/serializers.py b/MisplaceAI/process_misplaced_manager/serializers.py index 56877789c63cfe3043b24ac3ff194d9dda880f84..393f30c4e48edf08130c71ecf208406ca9bc2985 100644 --- a/MisplaceAI/process_misplaced_manager/serializers.py +++ b/MisplaceAI/process_misplaced_manager/serializers.py @@ -6,7 +6,8 @@ from .models import UploadedImage, UploadedVideo class UploadedImageSerializer(serializers.ModelSerializer): class Meta: model = UploadedImage - fields = ['id', 'image', 'uploaded_at'] + fields = ['id', 'image', 'uploaded_at', 'user'] + class UploadedVideoSerializer(serializers.ModelSerializer): class Meta: diff --git a/MisplaceAI/process_misplaced_manager/urls.py b/MisplaceAI/process_misplaced_manager/urls.py index 4da3c06816d5ad0c808dee37e9f197ca252906bb..0b19a737837e0525c67d157d57d4220e7724640c 100644 --- a/MisplaceAI/process_misplaced_manager/urls.py +++ b/MisplaceAI/process_misplaced_manager/urls.py @@ -4,7 +4,16 @@ from rest_framework.routers import DefaultRouter from .views import ( UploadedImageViewSet, UploadedVideoViewSet, normal_detection, - display_results, display_video_results,upload_video, download_image, delete_image,delete_video,download_media + display_results, + display_video_results,upload_video, + download_image, + delete_image, + delete_video, + download_media, + get_daily_limits, + set_daily_limits, + check_daily_limit, + increment_detection ) router = DefaultRouter() @@ -24,4 +33,10 @@ urlpatterns = [ path('delete-video/<str:video_name>/', delete_video, name='delete_video'), path('download_video/<str:file_path>/', download_media, name='download_media'), + path('daily-limits/', get_daily_limits, name='get_daily_limits'), + path('set-daily-limits/', set_daily_limits, name='set_daily_limits'), + path('check-daily-limit/', check_daily_limit, name='check_daily_limit'), + + path('increment-detection/', increment_detection, name='increment_detection'), + ] diff --git a/MisplaceAI/process_misplaced_manager/utils.py b/MisplaceAI/process_misplaced_manager/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7f6a32cd13556ef292decb1efd6bc6022b239ba9 --- /dev/null +++ b/MisplaceAI/process_misplaced_manager/utils.py @@ -0,0 +1,28 @@ +# MisplaceAI/process_misplaced_manager/utils.py + +from django.utils import timezone +from .models import DailyDetectionLimit, DetectionLimitSetting + +def check_daily_limit(user, detection_type): + limit_setting = DetectionLimitSetting.objects.first() + if not limit_setting: + limit_setting = DetectionLimitSetting.objects.create(daily_image_limit=10, daily_video_limit=5) + + detection_limit, created = DailyDetectionLimit.objects.get_or_create(user=user) + detection_limit.reset_limits() # Ensure limits are reset if the day has changed + + if detection_type == 'image': + if detection_limit.image_detection_count >= limit_setting.daily_image_limit: + return False, limit_setting.daily_image_limit - detection_limit.image_detection_count + elif detection_type == 'video': + if detection_limit.video_detection_count >= limit_setting.daily_video_limit: + return False, limit_setting.daily_video_limit - detection_limit.video_detection_count + return True, limit_setting.daily_image_limit - detection_limit.image_detection_count if detection_type == 'image' else limit_setting.daily_video_limit - detection_limit.video_detection_count + +def increment_detection_count(user, detection_type): + detection_limit = DailyDetectionLimit.objects.get(user=user) + if detection_type == 'image': + detection_limit.image_detection_count += 1 + elif detection_type == 'video': + detection_limit.video_detection_count += 1 + detection_limit.save() diff --git a/MisplaceAI/process_misplaced_manager/views.py b/MisplaceAI/process_misplaced_manager/views.py index c8264564e099c49a0537bcf064ab98b0c315aaf8..a3c7178a5c84757a91fb6d3bcddbd4d36ad4dc05 100644 --- a/MisplaceAI/process_misplaced_manager/views.py +++ b/MisplaceAI/process_misplaced_manager/views.py @@ -2,10 +2,10 @@ from rest_framework import viewsets, status from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.response import Response from django.shortcuts import get_object_or_404 -from .models import UploadedImage, UploadedVideo, UserVideoFramePreference +from .models import UploadedImage, UploadedVideo, UserVideoFramePreference,DetectionLimitSetting, DailyDetectionLimit from .serializers import UploadedImageSerializer, UploadedVideoSerializer from item_detector.utils import run_inference, load_model, create_category_index_from_labelmap from placement_rules.utils import PlacementRules @@ -20,6 +20,8 @@ from django.conf import settings from django.http import JsonResponse, HttpResponse, Http404 import numpy as np from moviepy.editor import VideoFileClip, ImageSequenceClip +from .utils import increment_detection_count + logger = logging.getLogger(__name__) @@ -340,3 +342,67 @@ def download_media(request, file_path): return response else: raise Http404 + +################################################################################################# +####################################### LIMITS PER USER ######################################### +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_daily_limits(request): + limit_setting = DetectionLimitSetting.objects.first() + if not limit_setting: + limit_setting = DetectionLimitSetting.objects.create(daily_image_limit=10, daily_video_limit=5) + return Response({ + 'daily_image_limit': limit_setting.daily_image_limit, + 'daily_video_limit': limit_setting.daily_video_limit + }) + +@api_view(['POST']) +@permission_classes([IsAdminUser]) +def set_daily_limits(request): + daily_image_limit = request.data.get('daily_image_limit', 10) + daily_video_limit = request.data.get('daily_video_limit', 5) + limit_setting, created = DetectionLimitSetting.objects.get_or_create(id=1) + limit_setting.daily_image_limit = daily_image_limit + limit_setting.daily_video_limit = daily_video_limit + limit_setting.save() + return Response({ + 'daily_image_limit': limit_setting.daily_image_limit, + 'daily_video_limit': limit_setting.daily_video_limit + }) + + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def check_daily_limit(request): + user = request.user + detection_type = request.GET.get('type') + + if detection_type not in ['image', 'video']: + return Response({'error': 'Invalid detection type'}, status=400) + + limit_setting = DetectionLimitSetting.objects.first() + detection_limit, created = DailyDetectionLimit.objects.get_or_create(user=user) + detection_limit.reset_limits() # Ensure limits are reset if the day has changed + + if detection_type == 'image': + remaining = limit_setting.daily_image_limit - detection_limit.image_detection_count + else: + remaining = limit_setting.daily_video_limit - detection_limit.video_detection_count + + return Response({ + 'remaining': remaining, + 'limit': limit_setting.daily_image_limit if detection_type == 'image' else limit_setting.daily_video_limit + }) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def increment_detection(request): + user = request.user + detection_type = request.data.get('type') + + if detection_type not in ['image', 'video']: + return Response({'error': 'Invalid detection type'}, status=400) + + increment_detection_count(user, detection_type) + return Response({'status': 'success'}) \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 5a0f1cd1070779e1b2d2e2cc6d72d2ec210e990c..231db48325ababe277aa8fa5a27c8bd694548f7c 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -16,6 +16,7 @@ import NormalDetectionPage from './pages/NormalDetection/NormalDetectionPage'; import UserDashboard from './pages/UserDashboard/UserDashboard'; import UserProfile from './pages/UserProfile/UserProfile'; import VideoDetectionPage from './pages/Video/VideoDetectionPage'; +import ManageDailyLimit from './pages/Admin/ManageDailyLimit'; import Error500 from './pages/Error/Error500/Error500'; import ProtectedRoute from './firewall/ProtectedRoute'; @@ -64,6 +65,11 @@ function App() { <ManageRulesPage /> </ProtectedRoute> } /> + <Route path="/admin/manage-daily-limit" element={ + <ProtectedRoute isAdminRoute={true}> + <ManageDailyLimit /> + </ProtectedRoute> + } /> <Route path="/detection-options" element={ <ProtectedRoute> <DetectionOptionsPage /> diff --git a/frontend/src/pages/Admin/AdminDashboard.js b/frontend/src/pages/Admin/AdminDashboard.js index 4482d17acd88095749ad1fbf76506c747fa7a52c..ddc9eb863aacc7d8a78d57a8221dc7f0ba249621 100644 --- a/frontend/src/pages/Admin/AdminDashboard.js +++ b/frontend/src/pages/Admin/AdminDashboard.js @@ -16,6 +16,7 @@ const AdminDashboard = () => { <Link to="/admin/Manage-Locations" className="list-group-item list-group-item-action">Manage Locations</Link> <Link to="/admin/manage-items" className="list-group-item list-group-item-action">Manage Items</Link> <Link to="/admin/manage-rules" className="list-group-item list-group-item-action">Manage Rules</Link> + <Link to="/admin/manage-daily-limit" className="list-group-item list-group-item-action">Manage Daily Detection Limit</Link> </div> </div> </div> diff --git a/frontend/src/pages/Admin/ManageDailyLimit.js b/frontend/src/pages/Admin/ManageDailyLimit.js new file mode 100644 index 0000000000000000000000000000000000000000..820ee094c5cc60f4d1de4756d41128c1a317484c --- /dev/null +++ b/frontend/src/pages/Admin/ManageDailyLimit.js @@ -0,0 +1,60 @@ +// src/pages/Admin/ManageDailyLimit.js + +import React, { useEffect, useState } from 'react'; +import { getDailyLimits, setDailyLimits } from '../../services/dailyLimitApi'; + +const ManageDailyLimit = () => { + const [dailyImageLimit, setDailyImageLimit] = useState(10); + const [dailyVideoLimit, setDailyVideoLimit] = useState(5); + + useEffect(() => { + const fetchDailyLimits = async () => { + const data = await getDailyLimits(); + setDailyImageLimit(data.daily_image_limit); + setDailyVideoLimit(data.daily_video_limit); + }; + fetchDailyLimits(); + }, []); + + const handleImageLimitChange = (e) => { + setDailyImageLimit(e.target.value); + }; + + const handleVideoLimitChange = (e) => { + setDailyVideoLimit(e.target.value); + }; + + const handleSave = async () => { + await setDailyLimits(dailyImageLimit, dailyVideoLimit); + alert('Daily limits updated successfully'); + }; + + return ( + <div className="pages-container-center"> + <h1>Manage Daily Detection Limits</h1> + <div className="form-group"> + <label htmlFor="dailyImageLimit">Daily Image Limit:</label> + <input + type="number" + id="dailyImageLimit" + value={dailyImageLimit} + onChange={handleImageLimitChange} + className="form-control" + /> + </div> + <div className="form-group"> + <label htmlFor="dailyVideoLimit">Daily Video Limit:</label> + <input + type="number" + id="dailyVideoLimit" + value={dailyVideoLimit} + onChange={handleVideoLimitChange} + className="form-control" + /> + </div> + <button onClick={handleSave} className="btn btn-primary">Save</button> + </div> + ); +}; + +export default ManageDailyLimit; diff --git a/frontend/src/pages/NormalDetection/NormalDetectionPage.js b/frontend/src/pages/NormalDetection/NormalDetectionPage.js index d86c331a11a4fff70ff54bab14291770f997d0bc..601d1198d11c9ac264718f60ebaabfb740805923 100644 --- a/frontend/src/pages/NormalDetection/NormalDetectionPage.js +++ b/frontend/src/pages/NormalDetection/NormalDetectionPage.js @@ -1,8 +1,10 @@ // src/pages/NormalDetection/NormalDetectionPage.js + import React, { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { Modal, Button } from 'react-bootstrap'; import { normalDetection, downloadImage, deleteImageByName } from '../../services/processMisplacedManagerApi'; +import { checkDailyLimit, incrementDetection } from '../../services/dailyLimitApi'; import '../../styles/main.css'; import LoadingIndicator from '../../components/detection/LoadingIndicator'; import DetectionResults from '../../components/detection/DetectionResults'; @@ -17,9 +19,18 @@ const NormalDetectionPage = () => { const [showModal, setShowModal] = useState(false); const [detectionComplete, setDetectionComplete] = useState(false); const [imageName, setImageName] = useState(null); + const [limitInfo, setLimitInfo] = useState({ remaining: 0, limit: 0 }); const navigate = useNavigate(); const location = useLocation(); + useEffect(() => { + const fetchLimitInfo = async () => { + const data = await checkDailyLimit('image'); + setLimitInfo(data); + }; + fetchLimitInfo(); + }, []); + useEffect(() => { const handleBeforeUnload = async () => { console.log('Navigation event detected:'); @@ -74,6 +85,11 @@ const NormalDetectionPage = () => { const handleSubmit = async (event) => { event.preventDefault(); + if (limitInfo.remaining <= 0) { + alert('You have reached your daily limit for image detection.'); + return; + } + if (imageFile) { setIsLoading(true); const formData = new FormData(); @@ -86,6 +102,9 @@ const NormalDetectionPage = () => { setMisplacedObjects(response.misplaced_objects); setDetectionComplete(true); setImageName(response.output_image_url.split('/').pop()); // Set the image name + await incrementDetection('image'); // Increment the detection count + const data = await checkDailyLimit('image'); // Fetch updated limit info + setLimitInfo(data); } catch (error) { console.error('Upload failed', error); } finally { @@ -132,13 +151,16 @@ const NormalDetectionPage = () => { <DetectionContainer title="Upload Image for Normal Detection"> <LoadingIndicator isLoading={isLoading} message="Your photo is being processed, please wait..." /> {!isLoading && !detectionComplete && ( - <ImageUploadForm - handleFileChange={handleFileChange} - handleSubmit={handleSubmit} - handleCameraClick={handleCameraClick} - handleGalleryClick={handleGalleryClick} - isLoading={isLoading} - /> + <> + <p>You have {limitInfo.remaining} out of {limitInfo.limit} image detections remaining today.</p> + <ImageUploadForm + handleFileChange={handleFileChange} + handleSubmit={handleSubmit} + handleCameraClick={handleCameraClick} + handleGalleryClick={handleGalleryClick} + isLoading={isLoading} + /> + </> )} {!isLoading && detectionComplete && ( <> diff --git a/frontend/src/pages/Video/VideoDetectionPage.js b/frontend/src/pages/Video/VideoDetectionPage.js index 338fa7c834c1bb26dece0b09e508145f18716bce..f12b292366bcf151e0aee2361fbdd1b4dbe126f4 100644 --- a/frontend/src/pages/Video/VideoDetectionPage.js +++ b/frontend/src/pages/Video/VideoDetectionPage.js @@ -3,6 +3,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { uploadVideo, getVideoResults, deleteVideo, downloadMedia } from '../../services/processMisplacedManagerApi'; +import { checkDailyLimit, incrementDetection } from '../../services/dailyLimitApi'; import '../../styles/main.css'; import LoadingIndicator from '../../components/detection/LoadingIndicator'; import DetectionResults from '../../components/detection/DetectionResults'; @@ -17,10 +18,19 @@ const VideoDetectionPage = () => { const [isLoading, setIsLoading] = useState(false); const [videoName, setVideoName] = useState(null); const [annotatedVideoName, setAnnotatedVideoName] = useState(null); + const [limitInfo, setLimitInfo] = useState({ remaining: 0, limit: 0 }); const navigate = useNavigate(); const location = useLocation(); const [isDeleting, setIsDeleting] = useState(false); + useEffect(() => { + const fetchLimitInfo = async () => { + const data = await checkDailyLimit('video'); + setLimitInfo(data); + }; + fetchLimitInfo(); + }, []); + useEffect(() => { const handleBeforeUnload = async () => { if (!isDeleting && annotatedVideoName) { @@ -75,6 +85,11 @@ const VideoDetectionPage = () => { const handleSubmit = async (event) => { event.preventDefault(); + if (limitInfo.remaining <= 0) { + alert('You have reached your daily limit for video detection.'); + return; + } + if (videoFile) { setIsLoading(true); const formData = new FormData(); @@ -91,6 +106,9 @@ const VideoDetectionPage = () => { setResult(videoResults); setVideoName(uploadResponse.video.split('/').pop()); // Extract original video name from the response setAnnotatedVideoName(videoResults.output_video_url.split('/').pop()); // Extract annotated video name from the results + await incrementDetection('video'); // Increment the detection count + const data = await checkDailyLimit('video'); // Fetch updated limit info + setLimitInfo(data); } else { throw new Error('Upload response did not contain video ID.'); } @@ -125,14 +143,17 @@ const VideoDetectionPage = () => { <DetectionContainer title="Upload Video for Misplaced Items Detection"> <LoadingIndicator isLoading={isLoading} message="Your video is being processed, please wait..." /> {!isLoading && ( - <UploadForm - handleFileChange={handleFileChange} - handleSubmit={handleSubmit} - handleFrameIntervalChange={handleFrameIntervalChange} - handleCameraClick={() => { }} - handleGalleryClick={() => { }} - isLoading={isLoading} - /> + <> + <p>You have {limitInfo.remaining} out of {limitInfo.limit} video detections remaining today.</p> + <UploadForm + handleFileChange={handleFileChange} + handleSubmit={handleSubmit} + handleFrameIntervalChange={handleFrameIntervalChange} + handleCameraClick={() => { }} + handleGalleryClick={() => { }} + isLoading={isLoading} + /> + </> )} <DetectionResults result={result} /> <div className="text-center mt-4"> diff --git a/frontend/src/services/dailyLimitApi.js b/frontend/src/services/dailyLimitApi.js new file mode 100644 index 0000000000000000000000000000000000000000..fade23698a3b781cf6b8246bd74f5a8a9c1a51ba --- /dev/null +++ b/frontend/src/services/dailyLimitApi.js @@ -0,0 +1,26 @@ +// src/services/dailyLimitApi.js + +import api from './api'; + +export const getDailyLimits = async () => { + const response = await api.get('/api/process_misplaced_manager/daily-limits/'); + return response.data; +}; + +export const setDailyLimits = async (dailyImageLimit, dailyVideoLimit) => { + const response = await api.post('/api/process_misplaced_manager/set-daily-limits/', { + daily_image_limit: dailyImageLimit, + daily_video_limit: dailyVideoLimit + }); + return response.data; +}; + +export const checkDailyLimit = async (detectionType) => { + const response = await api.get(`/api/process_misplaced_manager/check-daily-limit/?type=${detectionType}`); + return response.data; +}; + +export const incrementDetection = async (detectionType) => { + const response = await api.post('/api/process_misplaced_manager/increment-detection/', { type: detectionType }); + return response.data; +};