diff --git a/streetup/assets/migrations/0013_alter_parkingspotinspection_verified_marking_quality_and_more.py b/streetup/assets/migrations/0013_alter_parkingspotinspection_verified_marking_quality_and_more.py new file mode 100644 index 0000000..a862d28 --- /dev/null +++ b/streetup/assets/migrations/0013_alter_parkingspotinspection_verified_marking_quality_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 6.0.7 on 2026-08-07 15:40 + +import assets.models.inspection +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0012_assetinspection_inspector_role_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='parkingspotinspection', + name='verified_marking_quality', + field=models.CharField(blank=True, choices=[('good', 'Bon état'), ('medium', 'Moyen'), ('damaged', 'Dégradé'), ('missing', 'Absent / Effacé')], max_length=50, null=True, verbose_name='Qualité du marquage constatée'), + ), + migrations.CreateModel( + name='InspectionDocument', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=assets.models.inspection.inspection_document_upload_path, verbose_name='Fichier / Photo / Document')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('inspection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='assets.assetinspection', verbose_name='Inspection')), + ], + options={ + 'verbose_name': "Document / Photo d'inspection", + 'verbose_name_plural': "Documents / Photos d'inspection", + }, + ), + ] diff --git a/streetup/assets/migrations/0014_alter_inspectiondocument_file.py b/streetup/assets/migrations/0014_alter_inspectiondocument_file.py new file mode 100644 index 0000000..2a04126 --- /dev/null +++ b/streetup/assets/migrations/0014_alter_inspectiondocument_file.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0.7 on 2026-08-07 15:46 + +import assets.models.inspection +import common.storages +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0013_alter_parkingspotinspection_verified_marking_quality_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='inspectiondocument', + name='file', + field=models.FileField(storage=common.storages.PrivateMediaStorage(), upload_to=assets.models.inspection.inspection_document_upload_path, verbose_name='Fichier / Photo / Document'), + ), + ] diff --git a/streetup/assets/models/inspection.py b/streetup/assets/models/inspection.py index 58fbdfb..7cdd0c0 100644 --- a/streetup/assets/models/inspection.py +++ b/streetup/assets/models/inspection.py @@ -4,6 +4,9 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from common.storages import PrivateMediaStorage + +private_storage = PrivateMediaStorage() INSPECTION_RESULT_CHOICES = ( ('compliant', _('Conforme')), @@ -144,6 +147,82 @@ class AssetInspection(models.Model): return str(self.asset) return f"Cible #{self.asset_object_id}" + @property + def thematic(self): + from assets.permissions import _get_asset_thematic + if self.asset: + return _get_asset_thematic(self.asset) + return None + + @property + def thematic_icon(self): + th = self.thematic + if th and hasattr(th, 'icon') and th.icon: + return th.icon + return 'bi-geo-alt' + + @property + def detailed_inspection(self): + """Downcast à la sous-classe spécialisée si disponible (ex: ParkingSpotInspection).""" + if hasattr(self, 'parkingspotinspection'): + return self.parkingspotinspection + return self + + @property + def details_summary(self): + """Résumé textuel concis des constatations spécifiques de l'inspection.""" + leaf = self.detailed_inspection + parts = [] + + if hasattr(leaf, 'verified_standard_stand_count') and leaf.verified_standard_stand_count is not None: + parts.append(str(_("%(count)d arceaux classiques") % {'count': leaf.verified_standard_stand_count})) + if hasattr(leaf, 'verified_cargo_stand_count') and leaf.verified_cargo_stand_count is not None: + parts.append(str(_("%(count)d arceaux cargo") % {'count': leaf.verified_cargo_stand_count})) + if hasattr(leaf, 'verified_has_scooter_drop_zone') and leaf.verified_has_scooter_drop_zone is not None: + parts.append(str(_("Drop-zone trottinette") if leaf.verified_has_scooter_drop_zone else _("Sans drop-zone"))) + if hasattr(leaf, 'verified_marking_quality') and leaf.verified_marking_quality: + marking_labels = { + 'good': _("Marquage bon état"), + 'medium': _("Marquage moyen"), + 'damaged': _("Marquage dégradé"), + 'missing': _("Marquage absent / effacé"), + 'bad': _("Marquage mauvais"), + } + val = marking_labels.get(leaf.verified_marking_quality, leaf.verified_marking_quality) + parts.append(str(val)) + + if leaf.verified_obsolescence: + parts.append(str(_("Obsolescence : %s") % leaf.verified_obsolescence)) + + return ", ".join(parts) if parts else "-" + + def get_asset_url(self, is_mobile=False): + """Retourne l'URL de détail de l'asset cible (route mobile /mobile/... si is_mobile est True).""" + if not self.asset: + return None + if is_mobile: + from django.urls import reverse + from assets.permissions import _get_asset_thematic + th = _get_asset_thematic(self.asset) + thematic_code = th.code if th else 'parking' + asset_model = self.asset.__class__.__name__.lower() + try: + return reverse('mobile:mobile_asset_detail', kwargs={ + 'thematic_code': thematic_code, + 'asset_model': asset_model, + 'asset_id': self.asset.id + }) + except Exception: + pass + if hasattr(self.asset, 'get_absolute_url'): + return self.asset.get_absolute_url() + return None + + @property + def mobile_asset_url(self): + """Propriété raccourcie pour l'URL mobile de l'asset.""" + return self.get_asset_url(is_mobile=True) + def __str__(self): asset_str = str(self.asset) if self.asset else f"Asset #{self.asset_object_id}" date_str = self.inspection_date.strftime('%d/%m/%Y') if self.inspection_date else '' @@ -175,8 +254,15 @@ class ParkingSpotInspection(AssetInspection): blank=True, verbose_name=_("Drop zone trottinette constatée") ) + MARKING_QUALITY_CHOICES = ( + ('good', _("Bon état")), + ('medium', _("Moyen")), + ('damaged', _("Dégradé")), + ('missing', _("Absent / Effacé")), + ) verified_marking_quality = models.CharField( max_length=50, + choices=MARKING_QUALITY_CHOICES, null=True, blank=True, verbose_name=_("Qualité du marquage constatée") @@ -189,25 +275,46 @@ class ParkingSpotInspection(AssetInspection): def save(self, *args, **kwargs): super().save(*args, **kwargs) - if self.apply_changes_to_asset and self.asset and self.asset.__class__.__name__ == 'ParkingSpot': - target_asset = self.asset - modified = False - if self.verified_standard_stand_count is not None: - target_asset.standard_stand_count = self.verified_standard_stand_count - modified = True - if self.verified_cargo_stand_count is not None: - target_asset.cargo_stand_count = self.verified_cargo_stand_count - modified = True - if self.verified_stand_type is not None: - target_asset.stand_type = self.verified_stand_type - modified = True - if self.verified_has_scooter_drop_zone is not None: - target_asset.has_scooter_drop_zone = self.verified_has_scooter_drop_zone - modified = True - if self.verified_marking_quality is not None: - target_asset.marking_quality = self.verified_marking_quality - modified = True +def inspection_document_upload_path(instance, filename): + now = timezone.now() + year = now.strftime("%Y") + month = now.strftime("%m") + return f"inspections/documents/{year}/{month}/{filename}" - if modified: - target_asset.save() + +class InspectionDocument(models.Model): + """ + Document / Photo / Pièce jointe associée à une inspection d'asset. + """ + inspection = models.ForeignKey( + AssetInspection, + related_name='documents', + on_delete=models.CASCADE, + verbose_name=_("Inspection") + ) + file = models.FileField( + upload_to=inspection_document_upload_path, + storage=private_storage, + verbose_name=_("Fichier / Photo / Document") + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = _("Document / Photo d'inspection") + verbose_name_plural = _("Documents / Photos d'inspection") + + @property + def is_image(self): + if not self.file or not self.file.name: + return False + ext = self.file.name.split('.')[-1].lower() + return ext in ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'bmp'] + + @property + def filename(self): + import os + return os.path.basename(self.file.name) if self.file else "" + + def __str__(self): + return f"Document {self.filename} (Inspection #{self.inspection_id})" diff --git a/streetup/assets/permissions.py b/streetup/assets/permissions.py index 1619b2b..65bfcb5 100644 --- a/streetup/assets/permissions.py +++ b/streetup/assets/permissions.py @@ -268,6 +268,10 @@ def _get_asset_thematic(asset): if hasattr(asset, 'category') and asset.category: return asset.category.thematic + # Try direct thematic attribute + if hasattr(asset, 'thematic') and asset.thematic: + return asset.thematic + # Try from parent building category if hasattr(asset, 'building') and asset.building and hasattr(asset.building, 'category') and asset.building.category: return asset.building.category.thematic @@ -294,6 +298,9 @@ def _get_asset_thematic(asset): 'its': 'its', 'building': 'publicbuildings', 'publicbuilding': 'publicbuildings', + 'parking': 'parking', + 'parkingspot': 'parking', + 'parkinglocation': 'parking', } for prefix, thematic_code in thematic_mappings.items(): diff --git a/streetup/assets/static/assets/inspection_map.js b/streetup/assets/static/assets/inspection_map.js index 9a796c4..cc658e1 100644 --- a/streetup/assets/static/assets/inspection_map.js +++ b/streetup/assets/static/assets/inspection_map.js @@ -500,7 +500,11 @@ const modalEl = document.getElementById('quickInspectionModal'); if (!modalEl) return; - document.getElementById('insp_content_type_id').value = props.content_type_id || props.ct_id || 1; + let ctId = props.content_type_id || props.ct_id; + if (!ctId && (props.model_name === 'parkingspot' || props.spot_type || props.asset_type === 'parkingspot' || props.thematic === 'parking')) { + ctId = window.PARKING_SPOT_CT_ID; + } + document.getElementById('insp_content_type_id').value = ctId || ''; document.getElementById('insp_object_id').value = props.id || props.pk; document.getElementById('insp_target_type').textContent = props.asset_type_display || props.model_name || 'Asset'; @@ -512,18 +516,223 @@ if (parkingFields) { if (props.model_name === 'parkingspot' || props.spot_type) { parkingFields.classList.remove('d-none'); - document.getElementById('insp_std_stands').value = props.standard_stand_count || 0; - document.getElementById('insp_cargo_stands').value = props.cargo_stand_count || 0; + document.getElementById('insp_std_stands').value = ''; + document.getElementById('insp_cargo_stands').value = ''; + const markingSel = document.getElementById('insp_marking_quality'); + if (markingSel) markingSel.value = ''; } else { parkingFields.classList.add('d-none'); } } + // Réinitialisation de l'accumulateur de photos/fichiers + accumulatedInspectionFiles = []; + const previewContainer = document.getElementById('insp_photos_preview_container'); + if (previewContainer) previewContainer.innerHTML = ''; + const modal = new bootstrap.Modal(modalEl); modal.show(); }; + let accumulatedInspectionFiles = []; + let inspCameraStream = null; + + function initInspectionPhotoHandler() { + const btnCamera = document.getElementById('insp_btn_continuous_camera'); + const btnGallery = document.getElementById('insp_btn_gallery_files'); + const inputCamera = document.getElementById('insp_camera_input'); + const inputGallery = document.getElementById('insp_gallery_input'); + + const cameraModalEl = document.getElementById('inspCameraModal'); + const cameraPreview = document.getElementById('inspCameraPreview'); + const cameraCanvas = document.getElementById('inspCameraCanvas'); + const cameraFlashOverlay = document.getElementById('inspCameraFlashOverlay'); + const cameraStatus = document.getElementById('inspCameraStatus'); + const cameraCaptureBtn = document.getElementById('inspCameraCaptureBtn'); + const cameraStopBtn = document.getElementById('inspCameraStopBtn'); + + let cameraModal = null; + if (cameraModalEl && typeof bootstrap !== 'undefined' && bootstrap.Modal) { + cameraModal = bootstrap.Modal.getOrCreateInstance(cameraModalEl); + } + + if (!btnCamera || !btnGallery || !inputCamera || !inputGallery) return; + + function updateCameraStatus(msg, isError) { + if (!cameraStatus) return; + cameraStatus.textContent = msg || ''; + cameraStatus.className = isError ? 'small text-center text-danger my-2 flex-shrink-0' : 'small text-center text-info my-2 flex-shrink-0'; + } + + function triggerFlash() { + if (!cameraFlashOverlay) return; + cameraFlashOverlay.style.opacity = '1'; + setTimeout(() => { cameraFlashOverlay.style.opacity = '0'; }, 150); + } + + async function startCameraStream() { + if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) { + return false; + } + try { + updateCameraStatus('Initialisation de la caméra...', false); + inspCameraStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } }, + audio: false + }); + if ('srcObject' in cameraPreview) { + cameraPreview.srcObject = inspCameraStream; + } else { + cameraPreview.src = URL.createObjectURL(inspCameraStream); + } + cameraPreview.play(); + updateCameraStatus('Caméra prête. Appuyez sur Capturer pour enregistrer une photo.', false); + return true; + } catch (err) { + console.warn('Erreur caméra canvas:', err); + stopCameraStream(); + return false; + } + } + + function stopCameraStream() { + if (inspCameraStream) { + inspCameraStream.getTracks().forEach(t => t.stop()); + inspCameraStream = null; + } + if (cameraPreview) cameraPreview.srcObject = null; + updateCameraStatus('', false); + } + + function captureFrame() { + if (!cameraPreview || !cameraCanvas) return; + const w = cameraPreview.videoWidth; + const h = cameraPreview.videoHeight; + if (!w || !h) { + updateCameraStatus('La caméra n\'est pas encore prête.', true); + return; + } + cameraCanvas.width = w; + cameraCanvas.height = h; + const ctx = cameraCanvas.getContext('2d'); + ctx.drawImage(cameraPreview, 0, 0, w, h); + + triggerFlash(); + + cameraCanvas.toBlob(function(blob) { + if (!blob) return; + const filename = `inspection_photo_${Date.now()}.jpg`; + const file = new File([blob], filename, { type: 'image/jpeg' }); + accumulatedInspectionFiles.push(file); + renderPhotoPreviews(); + updateCameraStatus('Photo capturée avec succès !', false); + }, 'image/jpeg', 0.9); + } + + btnCamera.addEventListener('click', async function() { + if (cameraModal) { + const ok = await startCameraStream(); + if (ok) { + cameraModal.show(); + return; + } + } + // Fallback si le flux caméra (getUserMedia) n'est pas supporté + inputCamera.value = ''; + inputCamera.click(); + }); + + if (cameraCaptureBtn) { + cameraCaptureBtn.addEventListener('click', captureFrame); + } + + if (cameraStopBtn) { + cameraStopBtn.addEventListener('click', function() { + if (cameraModal) cameraModal.hide(); + }); + } + + if (cameraModalEl) { + cameraModalEl.addEventListener('hidden.bs.modal', function() { + stopCameraStream(); + }); + } + + btnGallery.addEventListener('click', function() { + inputGallery.value = ''; + inputGallery.click(); + }); + + function handleFileSelection(files) { + Array.from(files).forEach(file => { + accumulatedInspectionFiles.push(file); + }); + renderPhotoPreviews(); + } + + inputCamera.addEventListener('change', function(e) { + if (e.target.files && e.target.files.length > 0) { + handleFileSelection(e.target.files); + } + }); + + inputGallery.addEventListener('change', function(e) { + if (e.target.files && e.target.files.length > 0) { + handleFileSelection(e.target.files); + } + }); + + function renderPhotoPreviews() { + const previewContainer = document.getElementById('insp_photos_preview_container'); + if (!previewContainer) return; + previewContainer.innerHTML = ''; + + accumulatedInspectionFiles.forEach((file, index) => { + const col = document.createElement('div'); + col.className = 'col-4 col-sm-3 position-relative mt-2'; + + const card = document.createElement('div'); + card.className = 'border rounded overflow-hidden shadow-sm bg-white text-center p-1 position-relative'; + card.style.height = '75px'; + + if (file.type.startsWith('image/')) { + const img = document.createElement('img'); + img.src = URL.createObjectURL(file); + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + card.appendChild(img); + } else { + const icon = document.createElement('div'); + icon.className = 'd-flex flex-column align-items-center justify-content-center h-100 text-muted'; + icon.innerHTML = '' + file.name + ''; + card.appendChild(icon); + } + + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'btn btn-danger btn-sm rounded-circle position-absolute top-0 end-0 p-0 d-flex align-items-center justify-content-center'; + removeBtn.style.width = '20px'; + removeBtn.style.height = '20px'; + removeBtn.style.fontSize = '12px'; + removeBtn.style.transform = 'translate(30%, -30%)'; + removeBtn.innerHTML = '×'; + removeBtn.onclick = function(ev) { + ev.stopPropagation(); + accumulatedInspectionFiles.splice(index, 1); + renderPhotoPreviews(); + }; + + card.appendChild(removeBtn); + col.appendChild(card); + previewContainer.appendChild(col); + }); + } + } + function initFormHandler() { + initInspectionPhotoHandler(); + const form = document.getElementById('createInspectionForm') || document.getElementById('quickInspectionForm'); if (!form) return; @@ -542,6 +751,11 @@ const formData = new FormData(form); + // Ajout des fichiers accumulés en mode continu + accumulatedInspectionFiles.forEach(file => { + formData.append('photos', file); + }); + fetch(form.action, { method: 'POST', body: formData, diff --git a/streetup/assets/templates/assets/components/inspection_form_modal.html b/streetup/assets/templates/assets/components/inspection_form_modal.html index 29155d5..77990d5 100644 --- a/streetup/assets/templates/assets/components/inspection_form_modal.html +++ b/streetup/assets/templates/assets/components/inspection_form_modal.html @@ -11,7 +11,7 @@ -
+ {% csrf_token %} @@ -98,7 +98,7 @@
+ + +
+
+ {% translate "Photos & Fichiers" %} +
+
+
+ + +
+ + + + + + +
+
+
+ + + {% endblock %} diff --git a/streetup/assets/templates/assets/quick_inspection_map.html b/streetup/assets/templates/assets/quick_inspection_map.html index feeae04..42a1334 100644 --- a/streetup/assets/templates/assets/quick_inspection_map.html +++ b/streetup/assets/templates/assets/quick_inspection_map.html @@ -261,6 +261,9 @@ {% block extra_js %} - + {% endblock %} diff --git a/streetup/assets/tests_inspections_views.py b/streetup/assets/tests_inspections_views.py index 06dd0ca..67ec5d5 100644 --- a/streetup/assets/tests_inspections_views.py +++ b/streetup/assets/tests_inspections_views.py @@ -121,3 +121,25 @@ class InspectionViewsTestCase(TestCase): # 2. Tentative d'accès direct à quick_inspection_map -> HTTP 403 Forbidden resp_map = self.client.get(reverse('assets:quick_inspection_map')) self.assertEqual(resp_map.status_code, 403) + + def test_create_inspection_api_with_photo(self): + from django.core.files.uploadedfile import SimpleUploadedFile + test_photo = SimpleUploadedFile("test_inspection.jpg", b"fake_image_bytes", content_type="image/jpeg") + + response = self.client.post(reverse('assets:create_inspection_api'), { + 'content_type_id': self.spot_ct.id, + 'object_id': self.spot.id, + 'result_status': 'compliant', + 'nature': 'visual', + 'notes': 'Inspection avec photo', + 'photos': [test_photo], + }) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertTrue(data['success']) + + inspection = AssetInspection.objects.get(id=data['inspection_id']) + self.assertEqual(inspection.documents.count(), 1) + doc = inspection.documents.first() + self.assertTrue(doc.is_image) + self.assertTrue(doc.filename.startswith('test_inspection')) diff --git a/streetup/assets/views/core.py b/streetup/assets/views/core.py index 372901a..06fbafb 100644 --- a/streetup/assets/views/core.py +++ b/streetup/assets/views/core.py @@ -184,7 +184,12 @@ def get_asset_edit_context(request, asset, content_type): except Exception: pass + is_mobile = request.path.startswith('/mobile/') or request.GET.get('v') == 'mobile' or request.GET.get('mobile') == '1' + base_template = 'mobile/mobile_base.html' if is_mobile else 'base.html' + return { + 'base_template': base_template, + 'is_mobile': is_mobile, 'allowed_update_fields': allowed_update_fields, 'allowed_actions': allowed_actions, 'status_choices': status_choices, diff --git a/streetup/assets/views/inspections.py b/streetup/assets/views/inspections.py index eb569f9..feabe3f 100644 --- a/streetup/assets/views/inspections.py +++ b/streetup/assets/views/inspections.py @@ -35,7 +35,7 @@ def inspections_list(request): nature_filter = request.GET.get('nature', '') search_q = request.GET.get('q', '').strip() - qs = AssetInspection.objects.all().select_related('asset_content_type', 'inspector').order_by('-inspection_date') + qs = AssetInspection.objects.all().select_related('asset_content_type', 'inspector').prefetch_related('documents').order_by('-inspection_date') if filter_type == 'my': qs = qs.filter(inspector=request.user) @@ -101,11 +101,15 @@ def quick_inspection_map(request): is_mobile = request.path.startswith('/mobile/') base_template = "mobile/mobile_base.html" if is_mobile else "base.html" + from assets.models import ParkingSpot + parking_spot_ct_id = ContentType.objects.get_for_model(ParkingSpot).id + context = { 'base_template': base_template, 'is_mobile': is_mobile, 'thematics': thematics, 'categories': categories, + 'parking_spot_ct_id': parking_spot_ct_id, } return render(request, 'assets/quick_inspection_map.html', context) @@ -173,17 +177,28 @@ def create_inspection_api(request): nature=nature, inspector_role=inspector_role, verified_obsolescence=verified_obsolescence, - notes=notes, apply_changes_to_asset=apply_changes, ) + # Enregistrement des photos et documents joints + uploaded_files = request.FILES.getlist('photos') + request.FILES.getlist('documents') + request.FILES.getlist('files') + if uploaded_files: + from assets.models.inspection import InspectionDocument + for f in uploaded_files: + InspectionDocument.objects.create( + inspection=inspection, + file=f + ) + created_intervention_code = None if request.POST.get('create_intervention') == 'true' and result_status in ('non_compliant', 'damaged', 'missing'): - thematic = None - if hasattr(target_obj, 'category') and target_obj.category: - thematic = target_obj.category.thematic - elif hasattr(target_obj, 'thematic'): - thematic = target_obj.thematic + from assets.permissions import _get_asset_thematic + thematic = _get_asset_thematic(target_obj) + if not thematic: + if is_parking_spot: + thematic = Thematic.objects.filter(code='parking').first() + else: + thematic = Thematic.objects.first() from interventions.models import Intervention, InterventionAsset intervention = Intervention.objects.create( diff --git a/streetup/assets/views/parking.py b/streetup/assets/views/parking.py index eab10b3..3bf9b80 100644 --- a/streetup/assets/views/parking.py +++ b/streetup/assets/views/parking.py @@ -154,9 +154,13 @@ def parking_assets_geojson(request): except Exception: pass + spot_ct_id = ContentType.objects.get_for_model(ParkingSpot).id + properties = { "id": spot.id, "code": spot.code, + "content_type_id": spot_ct_id, + "model_name": "parkingspot", "name_fr": spot.get_name(), "name_nl": spot.name_nl or spot.name_fr or "", "lon": lon, diff --git a/streetup/assets/views/publiclighting.py b/streetup/assets/views/publiclighting.py index fb62dc6..2e7ba9b 100644 --- a/streetup/assets/views/publiclighting.py +++ b/streetup/assets/views/publiclighting.py @@ -376,12 +376,15 @@ def publiclighting_assets_geojson(request): def obj_to_feature(obj, asset_type=None): geometry, lon, lat = build_geometry_and_coords(obj) + ct_id = ContentType.objects.get_for_model(obj).id return { "type": "Feature", "id": obj.id, "geometry": geometry, "properties": { + "id": obj.id, "code": obj.code, + "content_type_id": ct_id, "name": obj.get_name(), "category": obj.category.get_name() if obj.category else None, "model": obj.model.get_name() if obj.model else None, diff --git a/streetup/assets/views/trafficlights.py b/streetup/assets/views/trafficlights.py index 76a6521..be3b9b9 100644 --- a/streetup/assets/views/trafficlights.py +++ b/streetup/assets/views/trafficlights.py @@ -967,8 +967,11 @@ def trafficlights_assets_geojson(request): def obj_to_feature(obj, asset_type=None): geometry, lon, lat = build_geometry_and_coords(obj) + ct_id = ContentType.objects.get_for_model(obj).id properties = { + "id": obj.id, "code": obj.code, + "content_type_id": ct_id, "name": obj.get_name(), "category": obj.category.get_name() if obj.category else None, "model": obj.model.get_name() if obj.model else None, diff --git a/streetup/mobile/templates/mobile/mobile_index.html b/streetup/mobile/templates/mobile/mobile_index.html index 344330b..ec77af6 100644 --- a/streetup/mobile/templates/mobile/mobile_index.html +++ b/streetup/mobile/templates/mobile/mobile_index.html @@ -92,6 +92,18 @@ {% endif %} + {% if can_inspect %} +
+ +
+ +
+

{% translate "Inspections d'assets" %}

+

{% translate "Consulter la liste et l'historique des vérifications d'assets" %}

+
+
+ {% endif %} + {% if can_edit_green_surfaces %}