Skip to content

Commit bbc913f

Browse files
authored
Merge pull request #21 from 223MapAction/dev
Dev
2 parents 35e2f55 + 5f394c8 commit bbc913f

File tree

4 files changed

+172
-121
lines changed

4 files changed

+172
-121
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Generated by Django 4.2.7 on 2025-08-13 15:42
2+
3+
import backend.supabase_storage
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('Mapapi', '0011_organisation_alter_evenement_audio_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='evenement',
16+
name='audio',
17+
field=models.FileField(blank=True, null=True, storage=backend.supabase_storage.VoiceStorage(), upload_to='events/'),
18+
),
19+
migrations.AlterField(
20+
model_name='evenement',
21+
name='photo',
22+
field=models.ImageField(blank=True, null=True, storage=backend.supabase_storage.ImageStorage(), upload_to='events/'),
23+
),
24+
migrations.AlterField(
25+
model_name='evenement',
26+
name='video',
27+
field=models.FileField(blank=True, null=True, storage=backend.supabase_storage.VideoStorage(), upload_to='events/'),
28+
),
29+
migrations.AlterField(
30+
model_name='incident',
31+
name='audio',
32+
field=models.FileField(blank=True, null=True, storage=backend.supabase_storage.VoiceStorage(), upload_to='incidents/'),
33+
),
34+
migrations.AlterField(
35+
model_name='incident',
36+
name='photo',
37+
field=models.ImageField(blank=True, null=True, storage=backend.supabase_storage.ImageStorage(), upload_to='incidents/'),
38+
),
39+
migrations.AlterField(
40+
model_name='incident',
41+
name='video',
42+
field=models.FileField(blank=True, null=True, storage=backend.supabase_storage.VideoStorage(), upload_to='incidents/'),
43+
),
44+
migrations.AlterField(
45+
model_name='rapport',
46+
name='file',
47+
field=models.FileField(blank=True, null=True, storage=backend.supabase_storage.ImageStorage(), upload_to='reports/'),
48+
),
49+
migrations.AlterField(
50+
model_name='user',
51+
name='avatar',
52+
field=models.ImageField(blank=True, default='avatars/default.png', null=True, storage=backend.supabase_storage.ImageStorage(), upload_to='avatars/'),
53+
),
54+
migrations.AlterField(
55+
model_name='user',
56+
name='organisation',
57+
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='organisation'),
58+
),
59+
migrations.AlterField(
60+
model_name='zone',
61+
name='photo',
62+
field=models.ImageField(blank=True, null=True, storage=backend.supabase_storage.ImageStorage(), upload_to='zones/'),
63+
),
64+
]

Mapapi/models.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,7 @@ class User(AbstractBaseUser, PermissionsMixin):
160160
community = models.ForeignKey('Communaute', db_column='user_communaute_id', related_name='user_communaute',
161161
on_delete=models.CASCADE, null=True, blank=True)
162162
provider = models.CharField(_('provider'), max_length=255, blank=True, null=True)
163-
organisation = models.ForeignKey(
164-
Organisation,
165-
on_delete=models.SET_NULL,
166-
null=True,
167-
blank=True,
168-
related_name="users"
169-
)
163+
organisation = models.CharField(_('organisation'), max_length=255, blank=True, null=True)
170164
points = models.IntegerField(null=True, blank=True, default=0)
171165
zones = models.ManyToManyField('Zone', blank=True)
172166
verification_token = models.UUIDField(default=uuid.uuid4, editable=False, null=True, blank=True)

Mapapi/serializer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
from rest_framework import serializers, generics, permissions, status
22
from .models import *
33

4-
class OrganisationSerializer(serializers.ModelSerializer):
5-
class Meta:
6-
model = Organisation
7-
fields = '__all__'
4+
85

96
from rest_framework import serializers
107
from django.contrib.auth import authenticate
@@ -27,7 +24,10 @@ class Meta:
2724
# if zones:
2825
# user.zones.set(zones)
2926
# return user
30-
27+
class OrganisationSerializer(serializers.ModelSerializer):
28+
class Meta:
29+
model = Organisation
30+
fields = '__all__'
3131

3232
class UserRegisterSerializer(serializers.ModelSerializer):
3333
class Meta:

backend/supabase_storage.py

Lines changed: 102 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1,178 @@
1+
# backend/supabase_storage.py
12
import os
2-
import uuid
3-
from urllib.parse import urljoin
43
from django.conf import settings
54
from django.core.files.storage import Storage
5+
from django.core.files.base import ContentFile
6+
from django.utils.deconstruct import deconstructible
7+
68
from supabase import create_client, Client
79
from storage3.utils import StorageException
810

11+
12+
@deconstructible
913
class SupabaseStorage(Storage):
1014
"""
11-
Custom storage backend for Supabase Storage
15+
Custom storage backend for Supabase Storage (déconstructible).
16+
Ne PAS mettre d'objet client dans __init__.
1217
"""
13-
def __init__(self, bucket_name=None):
14-
# Initialize Supabase client
15-
supabase_url = os.environ.get('SUPABASE_URL', '')
16-
supabase_key = os.environ.get('SUPABASE_ANON_KEY', '')
17-
18-
self.client: Client = create_client(supabase_url, supabase_key)
18+
def __init__(
19+
self,
20+
bucket_name=None,
21+
url_env="SUPABASE_URL",
22+
key_env="SUPABASE_ANON_KEY",
23+
signed_url_expiry=60 * 60 * 24 * 365, # 1 an
24+
):
25+
# ⚠️ Uniquement des types simples ici
1926
self.bucket_name = bucket_name
20-
27+
self.url_env = url_env
28+
self.key_env = key_env
29+
self.signed_url_expiry = signed_url_expiry
30+
# client non initialisé ici (lazy)
31+
32+
# Client Supabase créé à la volée (lazy) et non sérialisé dans la migration
33+
@property
34+
def client(self) -> Client:
35+
supabase_url = os.environ.get(self.url_env, "")
36+
supabase_key = os.environ.get(self.key_env, "")
37+
if not supabase_url or not supabase_key:
38+
raise RuntimeError("SUPABASE_URL/SUPABASE_ANON_KEY non définis dans l'environnement.")
39+
return create_client(supabase_url, supabase_key)
40+
2141
def _get_storage(self):
42+
if not self.bucket_name:
43+
raise RuntimeError("bucket_name non défini pour SupabaseStorage")
2244
return self.client.storage.from_(self.bucket_name)
23-
24-
def _open(self, name, mode='rb'):
25-
"""
26-
Retrieve the file from Supabase Storage
27-
"""
45+
46+
# Déconstruction explicite (facultative avec @deconstructible mais plus sûr)
47+
def deconstruct(self):
48+
path = "backend.supabase_storage.SupabaseStorage"
49+
args = []
50+
kwargs = {
51+
"bucket_name": self.bucket_name,
52+
"url_env": self.url_env,
53+
"key_env": self.key_env,
54+
"signed_url_expiry": self.signed_url_expiry,
55+
}
56+
return (path, args, kwargs)
57+
58+
def _open(self, name, mode="rb"):
2859
try:
29-
# Get the file contents
3060
response = self._get_storage().download(name)
31-
32-
# Create a file-like object
33-
from django.core.files.base import ContentFile
3461
return ContentFile(response)
35-
except StorageException as e:
36-
# Handle error, e.g., file not found
62+
except StorageException:
3763
raise FileNotFoundError(f"File {name} not found in bucket {self.bucket_name}")
3864

3965
def _ensure_folder_exists(self, path):
40-
"""
41-
Ensure that a folder exists in the bucket
42-
Supabase requires folders to exist before files can be uploaded to them
43-
"""
44-
if '/' in path:
45-
folder_path = path.rsplit('/', 1)[0] + '/'
66+
if "/" in path:
67+
folder_path = path.rsplit("/", 1)[0] + "/"
4668
try:
47-
# Check if folder exists by listing with prefix
48-
folders = self._get_storage().list(path=folder_path)
49-
# If we get here, the folder likely exists already
69+
_ = self._get_storage().list(path=folder_path)
5070
except StorageException:
51-
# Try to create the folder with an empty placeholder file
5271
try:
53-
self._get_storage().upload(folder_path + '.placeholder', b'')
72+
self._get_storage().upload(folder_path + ".placeholder", b"")
5473
except StorageException as e:
55-
# If folder already exists or we can't create it, just log and continue
74+
# non bloquant
5675
print(f"Note: Could not verify/create folder {folder_path}: {e}")
57-
76+
5877
def _save(self, name, content):
59-
"""
60-
Save the file to Supabase Storage in the appropriate folder path
61-
"""
6278
try:
63-
# Get the content as bytes
6479
file_content = content.read()
65-
66-
# Ensure the folder exists before uploading (if there's a path)
67-
if '/' in name:
80+
if "/" in name:
6881
self._ensure_folder_exists(name)
69-
70-
# Upload to Supabase with the full path
71-
result = self._get_storage().upload(name, file_content)
72-
73-
# Return the file path that was saved
82+
_ = self._get_storage().upload(name, file_content)
7483
return name
7584
except StorageException as e:
76-
# Handle upload error
7785
raise IOError(f"Error saving file to Supabase Storage: {e}")
7886

7987
def delete(self, name):
80-
"""
81-
Delete the file from Supabase Storage
82-
"""
8388
try:
8489
self._get_storage().remove([name])
8590
except StorageException:
86-
# File doesn't exist, pass silently
8791
pass
8892

8993
def exists(self, name):
90-
"""
91-
Check if a file exists in Supabase Storage
92-
"""
9394
try:
94-
# Get folder path and filename
95-
if '/' in name:
96-
folder_path = name.rsplit('/', 1)[0]
97-
filename = name.split('/')[-1]
98-
# List files in the specific folder
95+
if "/" in name:
96+
folder_path = name.rsplit("/", 1)[0]
97+
filename = name.split("/")[-1]
9998
files = self._get_storage().list(folder_path)
10099
else:
101-
# Files at bucket root
102100
files = self._get_storage().list()
103101
filename = name
104-
105-
# Check if file exists in the folder
106-
return any(file['name'] == filename for file in files)
102+
return any((f.get("name") or f.get("Name")) == filename for f in files)
107103
except StorageException:
108104
return False
109105

110106
def url(self, name):
111-
"""
112-
Return the public URL for a file
113-
"""
114107
try:
115-
# Use the sign endpoint instead of public as it's what Supabase now requires
116-
# The sign endpoint generates a URL with a token that allows access to the file
117-
return self._get_storage().create_signed_url(name, 60*60*24*365) # 1 year expiry
118-
except StorageException as e:
108+
signed = self._get_storage().create_signed_url(name, self.signed_url_expiry)
109+
# selon la version, la clé peut être 'signedURL' ou 'signed_url'
110+
if isinstance(signed, dict):
111+
return signed.get("signedURL") or signed.get("signed_url") or None
112+
return signed # fallback si lib renvoie directement une str
113+
except StorageException:
114+
# essai fallback naïf (inutile si path correct)
119115
try:
120-
# As fallback, try with just the filename
121-
if '/' in name:
122-
filename = name.split('/')[-1]
123-
return self._get_storage().create_signed_url(filename, 60*60*24*365)
124-
else:
125-
# Already tried with the name, so it truly failed
126-
return None
116+
filename = name.split("/")[-1]
117+
signed = self._get_storage().create_signed_url(filename, self.signed_url_expiry)
118+
if isinstance(signed, dict):
119+
return signed.get("signedURL") or signed.get("signed_url") or None
120+
return signed
127121
except StorageException:
128122
return None
129123

130124
def size(self, name):
131-
"""
132-
Return the size of a file
133-
"""
134125
try:
135-
# Get folder path and filename
136-
if '/' in name:
137-
folder_path = name.rsplit('/', 1)[0]
138-
filename = name.split('/')[-1]
139-
# List files in the specific folder
126+
if "/" in name:
127+
folder_path = name.rsplit("/", 1)[0]
128+
filename = name.split("/")[-1]
140129
files = self._get_storage().list(folder_path)
141130
else:
142-
# Files at bucket root
143131
files = self._get_storage().list()
144132
filename = name
145-
146-
# Find the file and get its size
147-
for file in files:
148-
if file['name'] == filename:
149-
return file.get('metadata', {}).get('size', 0)
133+
for f in files:
134+
if (f.get("name") or f.get("Name")) == filename:
135+
meta = f.get("metadata") or f.get("Metadata") or {}
136+
return meta.get("size") or meta.get("Size") or 0
150137
return 0
151138
except StorageException:
152139
return 0
153140

154141
def get_accessed_time(self, name):
155-
return None # Not supported by Supabase Storage
142+
return None
156143

157144
def get_created_time(self, name):
158-
return None # Not supported by Supabase Storage
145+
return None
159146

160147
def get_modified_time(self, name):
161-
return None # Not supported by Supabase Storage
148+
return None
162149

163150

151+
@deconstructible
164152
class ImageStorage(SupabaseStorage):
165-
"""
166-
Storage for images using the 'images' bucket
167-
"""
168-
def __init__(self):
169-
super().__init__(bucket_name='images')
153+
def __init__(self, **kwargs):
154+
super().__init__(bucket_name="images", **kwargs)
170155

156+
def deconstruct(self):
157+
path = "backend.supabase_storage.ImageStorage"
158+
return (path, [], {}) # pas d’args/kwargs car bucket fixé
171159

160+
161+
@deconstructible
172162
class VideoStorage(SupabaseStorage):
173-
"""
174-
Storage for videos using the 'videos' bucket
175-
"""
176-
def __init__(self):
177-
super().__init__(bucket_name='videos')
163+
def __init__(self, **kwargs):
164+
super().__init__(bucket_name="videos", **kwargs)
165+
166+
def deconstruct(self):
167+
path = "backend.supabase_storage.VideoStorage"
168+
return (path, [], {})
178169

179170

171+
@deconstructible
180172
class VoiceStorage(SupabaseStorage):
181-
"""
182-
Storage for audio files using the 'voices' bucket
183-
"""
184-
def __init__(self):
185-
super().__init__(bucket_name='voices')
173+
def __init__(self, **kwargs):
174+
super().__init__(bucket_name="voices", **kwargs)
175+
176+
def deconstruct(self):
177+
path = "backend.supabase_storage.VoiceStorage"
178+
return (path, [], {})

0 commit comments

Comments
 (0)