Skip to content

Commit

Permalink
Merge pull request #3212 from GeotrekCE/add_duplicate_action
Browse files Browse the repository at this point in the history
Add duplication on objects
  • Loading branch information
LePetitTim authored Jan 20, 2023
2 parents 48d9f9d + 8999182 commit 8adecb3
Show file tree
Hide file tree
Showing 27 changed files with 246 additions and 105 deletions.
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ CHANGELOG
2.94.0+dev (XXXX-XX-XX)
-----------------------

**New features**

- Add possibility to duplicate objects with geometries

**Minor improvements**

- Add blade type on signage detail view (#3325)
Expand Down Expand Up @@ -267,6 +271,7 @@ In preparation for HD Views developments (PR #3298)

- Add new setting `DIRECTION_ON_LINES_ENABLED` to have the `direction` field on lines instead of blades


2.87.2 (2022-09-23)
-----------------------

Expand Down
47 changes: 47 additions & 0 deletions geotrek/common/mixins/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
import hashlib
import os
import shutil
import uuid

from PIL.Image import DecompressionBombError
from django.conf import settings
from django.core.mail import mail_managers
from django.db import models
from django.db.models import Q, Max, Count

from django.template.defaultfilters import slugify
from django.template.loader import render_to_string
from django.utils.formats import date_format
Expand All @@ -21,6 +23,23 @@
from geotrek.common.mixins.managers import NoDeleteManager
from geotrek.common.utils import classproperty, logger

from mapentity.models import MapEntityMixin


class CheckBoxActionMixin:
@property
def checkbox(self):
return '<input type="checkbox" name="{}[]" value="{}" />'.format(self._meta.model_name,
self.pk)

@classproperty
def checkbox_verbose_name(cls):
return _("Action")

@property
def checkbox_display(self):
return self.checkbox


class TimeStampedModelMixin(models.Model):
# Computed values (managed at DB-level with triggers)
Expand Down Expand Up @@ -417,3 +436,31 @@ def add_property(cls, name, func, verbose_name):
raise AttributeError("%s has already an attribute %s" % (cls, name))
setattr(cls, name, property(func))
setattr(cls, '%s_verbose_name' % name, verbose_name)


def get_uuid_duplication(uid_field):
return uuid.uuid4()


class GeotrekMapEntityMixin(MapEntityMixin):
elements_duplication = {
"attachments": {"uuid": get_uuid_duplication},
"avoid_fields": ["aggregations"],
"uuid": get_uuid_duplication,
}

class Meta:
abstract = True

def duplicate(self, **kwargs):
elements_duplication = self.elements_duplication.copy()
if "name" in [field.name for field in self._meta.get_fields()]:
elements_duplication['name'] = f"{self.name} (copy)"
if "structure" in [field.name for field in self._meta.get_fields()]:
request = kwargs.pop('request', None)
if request:
elements_duplication['structure'] = request.user.profile.structure
clone = super(MapEntityMixin, self).duplicate(**elements_duplication)
if hasattr(clone, 'mutate'):
clone.mutate(self)
return clone
71 changes: 70 additions & 1 deletion geotrek/common/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from unittest import mock

from django.contrib import messages
from django.contrib.auth.models import Permission, User
from django.shortcuts import get_object_or_404
from django.test.utils import override_settings
Expand All @@ -15,7 +16,9 @@
# Workaround https://code.djangoproject.com/ticket/22865
from freezegun import freeze_time

from geotrek.common.models import FileType # NOQA
from geotrek.common.models import Attachment, AccessibilityAttachment, FileType # NOQA
from geotrek.common.tests.factories import AttachmentFactory, AttachmentAccessibilityFactory
from geotrek.common.utils.testdata import get_dummy_uploaded_image

from mapentity.tests.factories import SuperUserFactory, UserFactory
from mapentity.registry import app_settings
Expand Down Expand Up @@ -56,6 +59,72 @@ def test_document_public_booklet_export(self, mock_requests):
kwargs={'lang': 'en', 'pk': obj.pk, 'slug': obj.slug}))
self.assertEqual(response.status_code, 200)

@mock.patch('mapentity.helpers.requests')
def test_duplicate_object_without_structure(self, mock_requests):
if self.model is None or not getattr(self.model, 'can_duplicate') or hasattr(self.model, 'structure'):
return

obj_1 = self.modelfactory.create()
obj_1.refresh_from_db()
response_duplicate = self.client.post(
reverse(f'{self.model._meta.app_label}:{self.model._meta.model_name}_duplicate',
kwargs={"pk": obj_1.pk})
)

self.assertEqual(response_duplicate.status_code, 302)
self.client.get(response_duplicate['location'])
self.assertEqual(self.model.objects.count(), 2)
if 'name' in [field.name for field in self.model._meta.get_fields()]:
self.assertEqual(self.model.objects.filter(name__endswith='(copy)').count(), 2)
for field in self.model._meta.get_fields():
fields_name_different = ['id', 'uuid', 'date_insert', 'date_update', 'name', 'name_en']
if not field.related_model and field.name not in fields_name_different:
self.assertEqual(str(getattr(obj_1, field.name)), str(getattr(self.model.objects.last(), field.name)))

@mock.patch('mapentity.helpers.requests')
def test_duplicate_object_with_structure(self, mock_requests):
if self.model is None or not getattr(self.model, 'can_duplicate'):
return
fields_name = [field.name for field in self.model._meta.get_fields()]
if "structure" not in fields_name:
return
structure = StructureFactory.create()
obj_1 = self.modelfactory.create(structure=structure)
obj_1.refresh_from_db()

AttachmentFactory.create(content_object=obj_1,
attachment_file=get_dummy_uploaded_image())

attachments_accessibility = 'attachments_accessibility' in fields_name

if attachments_accessibility:
AttachmentAccessibilityFactory.create(content_object=obj_1,
attachment_accessibility_file=get_dummy_uploaded_image())
response = self.client.post(
reverse(f'{self.model._meta.app_label}:{self.model._meta.model_name}_duplicate',
kwargs={"pk": obj_1.pk})
)
self.assertEqual(response.status_code, 302)

msg = [str(message) for message in messages.get_messages(response.wsgi_request)]
self.assertEqual(msg[0],
f"{self.model._meta.verbose_name} has been duplicated successfully")

self.assertEqual(self.model.objects.count(), 2)
self.assertEqual(Attachment.objects.filter(object_id=obj_1.pk).count(), 1)
self.assertEqual(Attachment.objects.filter(object_id=self.model.objects.last().pk).count(), 1)
if attachments_accessibility:
self.assertEqual(AccessibilityAttachment.objects.filter(object_id=obj_1.pk).count(), 1)
self.assertEqual(AccessibilityAttachment.objects.filter(object_id=self.model.objects.last().pk).count(), 1)

if 'name' in fields_name:
self.assertEqual(self.model.objects.filter(name__endswith='(copy)').count(), 1)
self.assertEqual(self.model.objects.filter(structure=structure).count(), 1)
for field in self.model._meta.get_fields():
fields_name_different = ['id', 'uuid', 'date_insert', 'date_update', 'name', 'name_en']
if not field.related_model and field.name not in fields_name_different:
self.assertEqual(str(getattr(obj_1, field.name)), str(getattr(self.model.objects.last(), field.name)))

@mock.patch('mapentity.helpers.requests')
def test_document_public_export(self, mock_requests):
if self.model is None:
Expand Down
26 changes: 7 additions & 19 deletions geotrek/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@

from geotrek.altimetry.models import AltimetryMixin
from geotrek.authent.models import StructureRelated, StructureOrNoneRelated
from geotrek.common.mixins.models import TimeStampedModelMixin, NoDeleteMixin, AddPropertyMixin
from geotrek.common.utils import classproperty, sqlfunction, uniquify, simplify_coords
from geotrek.core.managers import PathManager, PathInvisibleManager, TopologyManager, PathAggregationManager, \
TrailManager
from geotrek.common.mixins.models import (TimeStampedModelMixin, NoDeleteMixin, AddPropertyMixin,
CheckBoxActionMixin, GeotrekMapEntityMixin)
from geotrek.common.utils import classproperty, simplify_coords, sqlfunction, uniquify
from geotrek.zoning.mixins import ZoningPropertiesMixin
from mapentity.models import MapEntityMixin
from mapentity.serializers import plain_text

logger = logging.getLogger(__name__)


class Path(ZoningPropertiesMixin, AddPropertyMixin, MapEntityMixin, AltimetryMixin,
class Path(CheckBoxActionMixin, ZoningPropertiesMixin, AddPropertyMixin, GeotrekMapEntityMixin, AltimetryMixin,
TimeStampedModelMixin, StructureRelated, ClusterableModel):
""" Path model. Spatial indexes disabled because managed in Meta.indexes """
geom = models.LineStringField(srid=settings.SRID, spatial_index=False)
Expand Down Expand Up @@ -74,6 +74,7 @@ class Path(ZoningPropertiesMixin, AddPropertyMixin, MapEntityMixin, AltimetryMix
include_invisible = PathInvisibleManager()

is_reversed = False
can_duplicate = False

@property
def topology_set(self):
Expand Down Expand Up @@ -113,7 +114,7 @@ def __str__(self):
class Meta:
verbose_name = _("Path")
verbose_name_plural = _("Paths")
permissions = MapEntityMixin._meta.permissions + [
permissions = GeotrekMapEntityMixin._meta.permissions + [
("add_draft_path", "Can add draft Path"),
("change_draft_path", "Can change draft Path"),
("delete_draft_path", "Can delete draft Path"),
Expand Down Expand Up @@ -302,19 +303,6 @@ def networks_display(self):
def get_create_label(cls):
return _("Add a new path")

@property
def checkbox(self):
return '<input type="checkbox" name="{}[]" value="{}" />'.format('path',
self.pk)

@classproperty
def checkbox_verbose_name(cls):
return _("Action")

@property
def checkbox_display(self):
return self.checkbox

def topologies_by_path(self, default_dict):
if 'geotrek.core' in settings.INSTALLED_APPS:
for trail in self.trails:
Expand Down Expand Up @@ -921,7 +909,7 @@ def __str__(self):
return self.network


class Trail(MapEntityMixin, Topology, StructureRelated):
class Trail(GeotrekMapEntityMixin, Topology, StructureRelated):
topo_object = models.OneToOneField(Topology, parent_link=True, on_delete=models.CASCADE)
name = models.CharField(verbose_name=_("Name"), max_length=64)
category = models.ForeignKey(
Expand Down
12 changes: 11 additions & 1 deletion geotrek/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
ComfortFactory, PathFactory, StakeFactory, TrailFactory, TrailCategoryFactory,
CertificationLabelFactory, CertificationStatusFactory, CertificationTrailFactory
)
from geotrek.core.models import Path, Trail
from geotrek.core.models import Path, PathAggregation, Trail


@skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only')
Expand Down Expand Up @@ -226,6 +226,16 @@ def test_trails_verbose_name(self):
path = PathFactory.create()
self.assertEqual(path.trails_verbose_name, 'Trails')

def test_trails_duplicate(self):
path_1 = PathFactory.create()
path_2 = PathFactory.create()
t = TrailFactory.create(paths=[path_1, path_2])
self.assertEqual(Trail.objects.count(), 1)
self.assertEqual(PathAggregation.objects.count(), 2)
t.duplicate()
self.assertEqual(PathAggregation.objects.count(), 4)
self.assertEqual(Trail.objects.count(), 2)


class TrailTestDisplay(TestCase):
def test_trails_certifications_display(self):
Expand Down
12 changes: 8 additions & 4 deletions geotrek/diving/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from django.utils.translation import get_language, gettext_lazy as _

from colorfield.fields import ColorField
from mapentity.models import MapEntityMixin

from geotrek.authent.models import StructureRelated
from geotrek.common.mixins.models import (NoDeleteMixin, TimeStampedModelMixin,
PublishableMixin, PicturesMixin, AddPropertyMixin,
PictogramMixin, OptionalPictogramMixin)
PictogramMixin, OptionalPictogramMixin, GeotrekMapEntityMixin,
get_uuid_duplication)
from geotrek.common.models import Theme
from geotrek.common.utils import intersecting, format_coordinates, spatial_reference
from geotrek.core.models import Topology
Expand Down Expand Up @@ -99,8 +99,8 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)


class Dive(ZoningPropertiesMixin, NoDeleteMixin, AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated,
TimeStampedModelMixin, PicturesMixin):
class Dive(ZoningPropertiesMixin, NoDeleteMixin, AddPropertyMixin, PublishableMixin,
GeotrekMapEntityMixin, StructureRelated, TimeStampedModelMixin, PicturesMixin):
description_teaser = models.TextField(verbose_name=_("Description teaser"), blank=True,
help_text=_("A brief summary"))
description = models.TextField(verbose_name=_("Description"), blank=True,
Expand All @@ -124,6 +124,10 @@ class Dive(ZoningPropertiesMixin, NoDeleteMixin, AddPropertyMixin, PublishableMi
portal = models.ManyToManyField('common.TargetPortal', blank=True, related_name='dives', verbose_name=_("Portal"))
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)

elements_duplication = {
"attachments": {"uuid": get_uuid_duplication}
}

class Meta:
verbose_name = _("Dive")
verbose_name_plural = _("Dives")
Expand Down
16 changes: 0 additions & 16 deletions geotrek/diving/templates/diving/dive_detail.html

This file was deleted.

2 changes: 1 addition & 1 deletion geotrek/diving/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def get_expected_datatables_attrs(self):
'id': self.obj.pk,
'levels': self.obj.levels_display,
'name': self.obj.name_display,
'thumbnail': 'None'
'thumbnail': 'None',
}

def get_bad_data(self):
Expand Down
5 changes: 2 additions & 3 deletions geotrek/feedback/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
from django.utils import timezone
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _
from mapentity.models import MapEntityMixin

from geotrek.common.mixins.models import AddPropertyMixin, NoDeleteMixin, PicturesMixin, TimeStampedModelMixin
from geotrek.common.mixins.models import AddPropertyMixin, NoDeleteMixin, PicturesMixin, TimeStampedModelMixin, GeotrekMapEntityMixin
from geotrek.common.utils import intersecting
from geotrek.core.models import Path
from geotrek.trekking.models import POI, Service, Trek
Expand Down Expand Up @@ -107,7 +106,7 @@ def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)


class Report(MapEntityMixin, PicturesMixin, TimeStampedModelMixin, NoDeleteMixin, AddPropertyMixin, ZoningPropertiesMixin):
class Report(GeotrekMapEntityMixin, PicturesMixin, TimeStampedModelMixin, NoDeleteMixin, AddPropertyMixin, ZoningPropertiesMixin):
"""User reports, submitted via *Geotrek-rando* or parsed from Suricate API."""

email = models.EmailField(verbose_name=_("Email"))
Expand Down
6 changes: 3 additions & 3 deletions geotrek/infrastructure/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from django.conf import settings

from extended_choices import Choices
from mapentity.models import MapEntityMixin

from geotrek.authent.models import StructureRelated, StructureOrNoneRelated
from geotrek.common.utils import classproperty
from geotrek.common.mixins.models import BasePublishableMixin, OptionalPictogramMixin, TimeStampedModelMixin
from geotrek.common.mixins.models import (BasePublishableMixin, OptionalPictogramMixin, TimeStampedModelMixin,
GeotrekMapEntityMixin)
from geotrek.core.models import Topology, Path
from geotrek.infrastructure.managers import InfrastructureGISManager

Expand Down Expand Up @@ -141,7 +141,7 @@ def distance(self, to_cls):
return settings.TREK_INFRASTRUCTURE_INTERSECTION_MARGIN


class Infrastructure(MapEntityMixin, BaseInfrastructure):
class Infrastructure(BaseInfrastructure, GeotrekMapEntityMixin):
""" An infrastructure in the park, which is not of type SIGNAGE """
type = models.ForeignKey(InfrastructureType, related_name="infrastructures", verbose_name=_("Type"), on_delete=models.CASCADE)
objects = InfrastructureGISManager()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{% extends "common/common_detail.html" %}
Loading

0 comments on commit 8adecb3

Please sign in to comment.