Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add duplication on objects #3212

Merged
merged 34 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
faa9a77
Add duplication on signage (test)
LePetitTim Aug 12, 2022
5bb2e4d
Add checkbox all objects
LePetitTim Aug 23, 2022
532d8ea
Fix duplicatemixin check same structure's property exist
LePetitTim Aug 23, 2022
a0a582b
Add test duplicate for each objects
LePetitTim Aug 23, 2022
e5a1d17
Add Action using template duplication on every list except project
LePetitTim Aug 23, 2022
ef0b130
Fix linting
LePetitTim Aug 24, 2022
ee0ef6c
Fix linting
LePetitTim Aug 24, 2022
ca45c38
Modify duplication model mixin
LePetitTim Aug 24, 2022
1e4444e
Change permissions
LePetitTim Aug 24, 2022
331771a
Fix linting
LePetitTim Aug 24, 2022
c5b20f6
Move duplication to detail from list
LePetitTim Aug 30, 2022
07fe289
Upgrade maepntity 8.4.0
LePetitTim Aug 30, 2022
a5d88c8
Change detail duplication
LePetitTim Aug 31, 2022
39292be
Change way of doing the duplication
LePetitTim Aug 31, 2022
41fadec
Remove useless column checkbox
LePetitTim Aug 31, 2022
8cb319c
Fix imports, blank line
LePetitTim Aug 31, 2022
6578d75
Add changelog
LePetitTim Aug 31, 2022
18d93d5
Create GeotrekMapentityMixin
LePetitTim Sep 2, 2022
1f65598
Remove geotrek duplication (in mapentity instead)
LePetitTim Sep 2, 2022
589e8e5
Remove detail common
LePetitTim Sep 2, 2022
2464cda
Use action duplicate mapentity, use geotrekmapentitymixin, remove old…
LePetitTim Sep 2, 2022
834fc69
Add structure user for new element, change name with copy
LePetitTim Oct 5, 2022
a886587
Fix imports duplication moved in mapentity
LePetitTim Oct 5, 2022
b91b284
Modify tests, remove tests already in mapentity
LePetitTim Oct 5, 2022
1907659
Use can_dupliacte to unallow duplication for paths and blades
LePetitTim Oct 5, 2022
346e378
Remove detail template signage and dive
LePetitTim Oct 5, 2022
1dfac35
Add duplication function for funding and manday (reverse foreign key)
LePetitTim Oct 5, 2022
fb5675e
Add attachmentacessibility duplication treks
LePetitTim Oct 5, 2022
a0f66d6
Change requirement, readd timestamped blade, remove duplicatemixin im…
LePetitTim Nov 28, 2022
18a381b
Remove import MapEntityDuplicate, Length
LePetitTim Nov 28, 2022
82337fd
fix: Remove duplication geotrek
LePetitTim Jan 5, 2023
181b7e5
fix: remove helper attachment (mapentity), remove import uuid trek model
LePetitTim Jan 16, 2023
b2c45bf
:bug: Add mutate for topologies
LePetitTim Jan 19, 2023
8999182
fix: remove delete mutate, not useful
LePetitTim Jan 19, 2023
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
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