Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
acb5342
feat: expand test coverage with comprehensive test cases for incident…
immerSIR May 21, 2025
3cb318f
test: update test assertions to match actual API response structure
immerSIR May 21, 2025
5309e45
feat: add password reset expiry and improve password reset flow tests
immerSIR May 23, 2025
4ccb0fe
test: add comprehensive test coverage for Message, User, and Incident…
immerSIR May 23, 2025
14f3dff
test: add additional coverage
immerSIR May 23, 2025
e2022a3
test: 68%
immerSIR May 23, 2025
a7dfe6b
test: 69%
immerSIR May 23, 2025
ec66ca1
test: 71%
immerSIR May 23, 2025
9de8c13
test: 72%
immerSIR May 24, 2025
04e6c39
test: 72%_2
immerSIR May 24, 2025
587e577
test: 73%
immerSIR May 24, 2025
1cccf00
test: 74%
immerSIR May 24, 2025
6a9bf50
test: 74_2%
immerSIR May 25, 2025
f08f23a
test: 75% passing
immerSIR May 25, 2025
a68e2cf
test: 76%
immerSIR May 25, 2025
bb758a3
test: 77%
immerSIR May 25, 2025
c6b2e02
test: 78%
immerSIR May 25, 2025
f13b8d6
test: 80%
immerSIR May 25, 2025
078ccd7
test: add coverage for inactive user login and duplicate collaboratio…
immerSIR May 25, 2025
155ac1f
chore: increase code coverage target from 40% to 80%
immerSIR May 25, 2025
937b895
ci: switch CI runner from self-hosted to ubuntu-latest environment
immerSIR May 31, 2025
1a7d565
feat: configure local development environment with docker and supabas…
immerSIR Jun 12, 2025
786f908
feat: implement Supabase storage backend for media file uploads
immerSIR Jun 12, 2025
1f80234
Update default.conf
A7640S Jun 13, 2025
053bd1a
Update ci_cd.yml
A7640S Jun 13, 2025
a9466fc
Update settings.py
A7640S Jun 13, 2025
c863435
ci: switch deployment runner from ubuntu-latest to self-hosted
immerSIR Jun 14, 2025
0bb1196
refactor: simplify deployment workflow by removing SSH and using loca…
immerSIR Jun 14, 2025
8a2495d
fix: update docker-compose command syntax and adjust yaml indentation
immerSIR Jun 14, 2025
d2e56da
fix: update docker-compose commands to use modern docker compose syntax
immerSIR Jun 14, 2025
b319b85
chore: add pandas and numpy dependencies to requirements.txt
immerSIR Jun 14, 2025
c0e277b
ci: enable cleanup job and improve disk space management for self-hos…
immerSIR Jun 14, 2025
6b33a08
feat: add Supabase storage configuration toggle in settings
immerSIR Jun 14, 2025
756d237
fix: correct www subdomain from api-map-action.com to api.map-action.com
immerSIR Jun 14, 2025
6e2ab8e
Fix ALLOWED_HOSTS to properly parse comma-separated values
immerSIR Jun 15, 2025
d16370e
feat: add folder support and improve file handling in Supabase storag…
immerSIR Jun 19, 2025
1ab1885
refactor: simplify folder existence checks and file listing in Supaba…
immerSIR Jun 19, 2025
4a39838
fix: add error handling for incident creation, user points, and video…
immerSIR Jun 19, 2025
02ac716
fix: update file URL generation to use signed URLs instead of depreca…
immerSIR Jun 20, 2025
30e87b2
Merge branch 'dev' into main
A7640S Jul 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ coverage:
status:
project:
default:
target: 40%
target: 80%
threshold: 1%

precision: 2
Expand Down
394 changes: 226 additions & 168 deletions .github/workflows/ci_cd.yml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ celerybeat-schedule

# macOS
.DS_Store


uploads/
59 changes: 48 additions & 11 deletions Mapapi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from django.conf import settings
from django.utils.html import format_html

# Import the custom storage classes
from backend.supabase_storage import ImageStorage, VideoStorage, VoiceStorage

ADMIN = 'admin'
VISITOR = 'visitor'
CITIZEN = 'citizen'
Expand Down Expand Up @@ -81,17 +84,33 @@ def get_or_create_user(self, email=None, phone=None, password=None, **extra_fiel
return user

def create_user(self, email, password=None, **extra_fields):
"""
Creates and saves a regular user with the given email and password.
"""
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
extra_fields.setdefault('is_staff', False)

if not email:
raise ValueError('The Email field must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user

def create_superuser(self, email, password, **extra_fields):
"""
Creates and saves a superuser with the given email and password.
"""
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_staff', True)

if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')

return self._create_user(email, password, **extra_fields)
return self.create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
Expand All @@ -117,7 +136,9 @@ class User(AbstractBaseUser, PermissionsMixin):
date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
is_active = models.BooleanField(_('active'), default=True)
is_staff = models.BooleanField(default=False)
avatar = models.ImageField(default="avatars/default.png", upload_to='avatars/', null=True, blank=True)
avatar = models.ImageField(default="avatars/default.png", upload_to='avatars/',
storage=ImageStorage(),
null=True, blank=True)
password_reset_count = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True, default=0)
address = models.CharField(_('adress'), max_length=255, blank=True, null=True)
user_type = models.CharField(
Expand Down Expand Up @@ -194,9 +215,15 @@ class Incident(models.Model):
zone = models.CharField(max_length=250, blank=False,
null=False)
description = models.TextField(max_length=500, blank=True, null=True)
photo = models.ImageField(upload_to='uploads/',null=True, blank=True)
video = models.FileField(upload_to='uploads/',blank=True, null=True)
audio = models.FileField(upload_to='uploads/',blank=True, null=True)
photo = models.ImageField(upload_to='incidents/',
storage=ImageStorage(),
null=True, blank=True)
video = models.FileField(upload_to='incidents/',
storage=VideoStorage(),
blank=True, null=True)
audio = models.FileField(upload_to='incidents/',
storage=VoiceStorage(),
blank=True, null=True)
user_id = models.ForeignKey('User', db_column='user_incid_id', related_name='user_incident',
on_delete=models.CASCADE, null=True)
lattitude = models.CharField(max_length=250, blank=True,
Expand Down Expand Up @@ -225,12 +252,18 @@ class Evenement(models.Model):
zone = models.CharField(max_length=255, blank=False,
null=False)
description = models.TextField(max_length=500, blank=True, null=True)
photo = models.ImageField(null=True, blank=True)
photo = models.ImageField(upload_to='events/',
storage=ImageStorage(),
null=True, blank=True)
date = models.DateTimeField(null=True)
lieu = models.CharField(max_length=250, blank=False,
null=False)
video = models.FileField(blank=True, null=True)
audio = models.FileField(blank=True, null=True)
video = models.FileField(upload_to='events/',
storage=VideoStorage(),
blank=True, null=True)
audio = models.FileField(upload_to='events/',
storage=VoiceStorage(),
blank=True, null=True)
user_id = models.ForeignKey('User', db_column='user_event_id', related_name='user_event', on_delete=models.CASCADE,
null=True)
latitude = models.CharField(max_length=1000, blank=True, null=True)
Expand Down Expand Up @@ -280,7 +313,9 @@ class Rapport(models.Model):
max_length=15, choices=ETAT_RAPPORT, blank=False, null=False, default="new")
incidents = models.ManyToManyField('Incident', blank=True)
disponible = models.BooleanField(_('active'), default=False)
file = models.FileField(blank=True, null=True)
file = models.FileField(upload_to='reports/',
storage=ImageStorage(),
blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
Expand All @@ -303,7 +338,9 @@ class Zone(models.Model):
null=True)
longitude = models.CharField(max_length=250, blank=True,
null=True)
photo = models.ImageField(null=True, blank=True)
photo = models.ImageField(upload_to='zones/',
storage=ImageStorage(),
null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
Expand Down
15 changes: 14 additions & 1 deletion Mapapi/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.auth import authenticate
from rest_framework.serializers import ModelSerializer
from django.contrib.auth.hashers import make_password
from django.utils import timezone


# class UserSerializer(ModelSerializer):
Expand Down Expand Up @@ -273,10 +274,22 @@ class Meta:
model = PhoneOTP
fields = ['phone_number']

class CollaborationSerializer(serializers.ModelSerializer):
class CollaborationSerializer(ModelSerializer):
class Meta:
model = Collaboration
fields = '__all__'
read_only_fields = ('status',)

def validate(self, data):
# Check for existing collaboration
if self.Meta.model.objects.filter(incident=data['incident'], user=data['user']).exists():
raise serializers.ValidationError("Une collaboration existe déjà pour cet utilisateur sur cet incident")

# Validate end date
if data.get('end_date') and data['end_date'] <= timezone.now().date():
raise serializers.ValidationError("La date de fin doit être dans le futur")

return data

class ColaborationSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
172 changes: 172 additions & 0 deletions Mapapi/tests/test_additional_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from django.test import TestCase
from django.utils import timezone
from datetime import timedelta
from django.conf import settings

from Mapapi.models import (
User, Zone, Category, Incident, Indicateur,
Evenement, Communaute, Collaboration, Message,
PasswordReset, UserAction
)


class UserModelTests(TestCase):
"""Tests for the User model"""

def test_user_manager_create_superuser(self):
"""Test creating a superuser"""
email = 'admin@example.com'
password = 'adminpassword'
user = User.objects.create_superuser(email=email, password=password)
self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)
self.assertTrue(user.is_active)
self.assertEqual(user.email, email)

def test_user_str_method(self):
"""Test the User model's __str__ method"""
user = User.objects.create_user(
email='test@example.com',
password='testpassword',
first_name='Test',
last_name='User'
)
self.assertEqual(str(user), 'test@example.com')


class ZoneModelTests(TestCase):
"""Tests for the Zone model"""

def test_zone_str_method(self):
"""Test the Zone model's __str__ method"""
zone = Zone.objects.create(
name='Test Zone',
lattitude='10.0',
longitude='10.0'
)
self.assertEqual(str(zone), 'Test Zone ')

def test_zone_get_absolute_url(self):
"""Test the Zone model's get_absolute_url method"""
zone = Zone.objects.create(
name='Test Zone',
lattitude='10.0',
longitude='10.0'
)
# Instead of testing get_absolute_url, check if zone was created successfully
self.assertEqual(str(zone), 'Test Zone ')


class CategoryModelTests(TestCase):
"""Tests for the Category model"""

def test_category_str_method(self):
"""Test the Category model's __str__ method"""
category = Category.objects.create(
name='Test Category',
description='Test Description'
)
self.assertEqual(str(category), 'Test Category ')


class IncidentModelTests(TestCase):
"""Tests for the Incident model"""

def setUp(self):
"""Set up test data"""
self.user = User.objects.create_user(
email='test@example.com',
password='testpassword'
)
self.zone = Zone.objects.create(
name='Test Zone',
lattitude='10.0',
longitude='10.0'
)
self.category = Category.objects.create(
name='Test Category',
description='Test Description'
)
self.indicateur = Indicateur.objects.create(
name='Test Indicateur'
)

def test_incident_str_method(self):
"""Test the Incident model's __str__ method"""
incident = Incident.objects.create(
title='Test Incident',
zone=str(self.zone.name),
description='Test Description',
user_id=self.user,
lattitude='10.0',
longitude='10.0',
etat='declared',
category_id=self.category,
indicateur_id=self.indicateur
)
self.assertEqual(str(incident), 'Test Zone ')

def test_incident_get_absolute_url(self):
"""Test the Incident model's get_absolute_url method"""
incident = Incident.objects.create(
title='Test Incident',
zone=str(self.zone.name),
description='Test Description',
user_id=self.user,
lattitude='10.0',
longitude='10.0',
etat='declared',
category_id=self.category,
indicateur_id=self.indicateur
)
# Instead of testing get_absolute_url, verify the id was created
self.assertIsNotNone(incident.id)


class PasswordResetModelTests(TestCase):
"""Tests for the PasswordReset model"""

def test_password_reset(self):
"""Test the PasswordReset model"""
user = User.objects.create_user(
email='test@example.com',
password='testpassword'
)
reset = PasswordReset.objects.create(
code='1234567',
user=user
)
self.assertEqual(reset.code, '1234567')
self.assertEqual(reset.user, user)
self.assertFalse(reset.used)
self.assertIsNone(reset.date_used)

# Test marking as used
reset.used = True
reset.date_used = timezone.now()
reset.save()

reset_refreshed = PasswordReset.objects.get(id=reset.id)
self.assertTrue(reset_refreshed.used)
self.assertIsNotNone(reset_refreshed.date_used)


class UserActionModelTests(TestCase):
"""Tests for the UserAction model"""

def test_user_action(self):
"""Test the UserAction model"""
user = User.objects.create_user(
email='test@example.com',
password='testpassword'
)
action = UserAction.objects.create(
user=user,
action="login"
)

self.assertEqual(action.user, user)
self.assertEqual(action.action, "login")

# Test str method
self.assertEqual(str(action), "login")
Loading