Skip to content

Commit

Permalink
✨ Allow to store custom map themes (authenticathed users) (#813)
Browse files Browse the repository at this point in the history
* Create model for customer themes.

* Implemented CRUD custom theme.

* Add constraint.

* Typo

* Add invalidation of project cache.

* Fix test.

* Remove CSFR check.

* ✨ Client
  g3w-suite/g3w-client@0e226b2

* 🐛 Client
  g3w-suite/g3w-client@37adf60

* Add pydantic validation data structure.

---------

Co-authored-by: wlorenzetti <lorenzett@gis3w.it>
Co-authored-by: volterra79 <boccacci.francesco@gmail.com>
Co-authored-by: Raruto <Raruto@users.noreply.github.com>
  • Loading branch information
4 people committed May 6, 2024
1 parent 005e0e5 commit 3d6637e
Showing 14 changed files with 465 additions and 40 deletions.
2 changes: 1 addition & 1 deletion g3w-admin/client/static/client/css/app.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion g3w-admin/client/static/client/js/app.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion g3w-admin/client/static/client/js/app.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion g3w-admin/editing/static/editing/js/plugin.js

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions g3w-admin/qdjango/api/projects/danticmodels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# coding=utf-8
"""" Pydantic models
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the Mozilla Public License 2.0.
"""

__author__ = 'lorenzetti@gis3w.it'
__date__ = '2024-04-17'
__copyright__ = 'Copyright 2015 - 2024, Gis3w'
__license__ = 'MPL 2.0'


from pydantic import BaseModel
from typing import List, Dict, Optional, Union


class TreeItemPDModel(BaseModel):
name: str
id: str
visible: bool


class TreeNodeItemPDModel(BaseModel):
name: str
#mutually-exclusive: Optional[bool] = None # Todo: replace in the future '-' with '_'
node: Optional[List[Union['TreeItemPDModel', TreeItemPDModel]]] = None
checked: bool
expanded: bool


class ThemeProjectPDModel(BaseModel):
layerstree: List[Union[TreeItemPDModel, TreeNodeItemPDModel]]
styles: Dict[str, str]
24 changes: 21 additions & 3 deletions g3w-admin/qdjango/api/projects/serializers.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,8 @@
SessionTokenFilter,
GeoConstraintRule,
MSG_LEVELS,
FilterLayerSaved
FilterLayerSaved,
CustomerTheme
)
from qdjango.utils.data import QGIS_LAYER_TYPE_NO_GEOM
from qdjango.utils.models import get_capabilities4layer, get_view_layer_ids
@@ -260,7 +261,13 @@ def set_map_themes(self, ret, qgs_project):
:return: None
"""

ret['map_themes'] = []
ret['map_themes'] = {
'project': [],
'custom': []
}

# Check for QGIS project themes
# -----------------------------
map_themes = qgs_project.mapThemeCollection().mapThemes()
if len(map_themes) == 0:
return
@@ -276,7 +283,18 @@ def set_map_themes(self, ret, qgs_project):
r.layer().id(): r.currentStyle
})

ret['map_themes'].append(theme)
ret['map_themes']['project'].append(theme)

# Check for custom themes
# -----------------------
if not self.request.user.is_anonymous:
c_themes = CustomerTheme.objects.filter(project_id=ret['id'], user=self.request.user)
for c_theme in c_themes:
ret['map_themes']['custom'].append({
'theme': c_theme.name,
'styles': c_theme.styles
})



def get_bookmarks(self, qgs_project):
107 changes: 97 additions & 10 deletions g3w-admin/qdjango/api/projects/views.py
Original file line number Diff line number Diff line change
@@ -10,21 +10,30 @@
__date__ = '2020-10-09'
__copyright__ = 'Copyright 2015 - 2020, Gis3w'

import json

from django.conf import settings
from django.shortcuts import get_object_or_404, Http404
from django.shortcuts import (
get_object_or_404,
Http404
)
from django.urls import reverse
from django.core.files.storage import default_storage
from core.api.authentication import CsrfExemptSessionAuthentication
from core.utils.response import send_file
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from guardian.shortcuts import get_anonymous_user
from core.api.base.views import G3WAPIView
from core.api.permissions import ProjectPermission
from qdjango.api.projects.permissions import ProjectIsActivePermission
from qdjango.apps import get_qgs_project
from qdjango.models import Project
from qdjango.models import (
Project, CustomerTheme
)
from qdjango.signals import reading_layer_model
from qdjango.utils.models import get_view_layer_ids
from .danticmodels import ThemeProjectPDModel


from osgeo import gdal, osr
@@ -33,7 +42,7 @@

import logging

logger = logging.getLogger(__name__)
logger = logging.getLogger('g3wadmin.debug')


class QdjangoWebServicesAPIview(G3WAPIView):
@@ -212,33 +221,111 @@ class QdjangoPrjThemeAPIview(G3WAPIView):
ProjectIsActivePermission
)

authentication_classes = (
CsrfExemptSessionAuthentication,
)

def _get_url_params(self, **kwargs):
self.project_id = kwargs['project_id']
self.theme_name = kwargs['theme_name']

def get(self, request, **kwargs):
self._get_url_params(**kwargs)
return self.layerstree(request, **kwargs)

def post(self, request, **kwargs):
return self.layerstree(request, **kwargs)
"""
Post action view for custom theme data
"""

# TODO: add theme data validation structure, use Pydantic?
self._get_url_params(**kwargs)

# Validate POST data

def layerstree(self, request, **kwargs):


try:

# Validate POST data
# ------------------
try:
ThemeProjectPDModel(**self.request.data)
except Exception as e:
logger.error(str(e))
raise ValidationError(str(e))

# CRUD
# ------------------------

data = json.dumps(self.request.data)

# Insert/get
c_theme, created = CustomerTheme.objects.get_or_create(
defaults={
'theme': data
},
project_id=self.project_id,
user=self.request.user,
name=self.theme_name
)

# Update
if not created:
c_theme.theme = data
c_theme.save()

except Exception as e:
self.results.error = str(e)
self.results.result = False

return Response(self.results.results)

def delete(self, request, **kwargs):
self._get_url_params(**kwargs)

try:
CustomerTheme.objects.get(project_id=self.project_id, user=self.request.user, name=self.theme_name).delete()
except Exception as e:
self.results.error = str(e)
self.results.result = False

return Response(self.results.results)

def layerstree(self, request, **kwargs):

try:

# Retrieve project qdjando instance and qgsproject instance
project = get_object_or_404(Project, pk=kwargs['project_id'])
project = get_object_or_404(Project, pk=self.project_id)
qgs_project = get_qgs_project(project.qgis_file.path)

# First check for custom theme
try:
c_theme = CustomerTheme.objects.get(project=project, user=self.request.user, name=self.theme_name)
self.results.results.update({
'data': c_theme.layerstree
})

return Response(self.results.results)

except:
logger.info(f'[CUSTOMER THEME]: The theme \'{self.theme_name}\' is not a custom theme '
f'for user {self.request.user} in the project {self.project_id}')
pass

# Validation theme name
theme_collections = qgs_project.mapThemeCollection()
map_themes = theme_collections.mapThemes()
theme_name = kwargs['theme_name']


if len(map_themes) == 0:
raise Exception(f"Themes are not available for project {project.title}")

if theme_name not in map_themes:
raise Exception(f"Theme name '{theme_name}' is not available!")
if self.theme_name not in map_themes:
raise Exception(f"Theme name '{self.theme_name}' is not available!")

map_theme = theme_collections.mapThemeState(theme_name)
map_theme = theme_collections.mapThemeState(self.theme_name)

# Get node group expanded anche checked
node_group_expanded = map_theme.expandedGroupNodes()
31 changes: 31 additions & 0 deletions g3w-admin/qdjango/migrations/0117_auto_20240416_0757.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.25 on 2024-04-16 07:57

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('qdjango', '0116_auto_20231204_1357'),
]

operations = [
migrations.AlterField(
model_name='project',
name='use_map_extent_as_init_extent',
field=models.BooleanField(default=False, verbose_name='Use QGIS project map start extent as webgis init extent'),
),
migrations.CreateModel(
name='CustomerTheme',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Theme name')),
('theme', models.TextField(verbose_name='JSON theme structure')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='qdjango.project')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.2.25 on 2024-04-16 14:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('qdjango', '0117_auto_20240416_0757'),
]

operations = [
migrations.AddConstraint(
model_name='customertheme',
constraint=models.UniqueConstraint(fields=('project', 'user', 'name'), name='unique_name_user'),
),
]
42 changes: 42 additions & 0 deletions g3w-admin/qdjango/models/projects.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

from django.utils.text import slugify
from django_extensions.db.fields import AutoSlugField
from django.db.models import UniqueConstraint
from ordered_model.models import OrderedModel
from core.configs import *
from core.mixins.models import G3WACLModelMixins, G3WProjectMixins
@@ -34,6 +35,8 @@
setPermissionUserObject,
)

import json

logger = logging.getLogger(__name__)

# Layer type with widget set capability
@@ -1412,3 +1415,42 @@ def _permissionsToViewers(self, users_id, mode='add'):
@staticmethod
def get_by_type(type='search'):
return Widget.objects.filter(widget_type=type)


class CustomerTheme(models.Model):
"""
Model to store custom Map Theme
"""

project = models.ForeignKey(Project, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(_('Theme name'), max_length=255)
theme = models.TextField(_('JSON theme structure'))

@property
def styles(self):
return json.loads(self.theme)['styles']

@property
def layerstree(self):
return json.loads(self.theme)['layerstree']

def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):

super().save(force_insert=force_insert, force_update=force_update, using=using,
update_fields=update_fields)

# Invalidate cache project
self.project.invalidate_cache(user=self.user)

def __str__(self):
return self.name

class Meta:
constraints = [
UniqueConstraint(
name='unique_name_user',
fields=['project', 'user', 'name']
)
]
Binary file modified g3w-admin/qdjango/tests/data/geodata/cascade_autorelation.sqlite
Binary file not shown.
Binary file modified g3w-admin/qdjango/tests/data/geodata/qgis_widget_test_data.gpkg
Binary file not shown.
Loading

0 comments on commit 3d6637e

Please sign in to comment.