Customize the application

The application can be customized with the following mechanisms: functionalities, UI metadata and data model extensions.

The functionalities will be attached to the role and the theme, and the UI metadata will be attached to all the elements of the theme.

They should be configured in the vars file, in the admin_interface / available_functionalities or respectively available_metadata. It is a list of objects which have a name and a type.

The type can be:

Check CONST_vars.yaml for examples of usage.

In order to inherit the default values from CONST_vars.yaml, make sure the update_paths section contains the item admin_interface.available_functionalities or respectively admin_interface.available_metadata.

URL

In the admin interface, we can use for all URL definitions the following special schema:

  • static: to use a static route,

    • static:///icon.png will get the URL of the static static route of the project.

    • static://custom-static/icon.png will get the URL of the custom-static static route of the project.

    • static://prj:img/icon.png will get the URL of the img static route of prj.

  • config: to get the server name from the URL, with the config from the vars file:

    servers:
       my_server: https://example.com/test
    

    config://my_server/icon.png will be transformed into the URL https://example.com/test/icon.png.

Functionalities

c2cgeoportal provides the concept of functionality that can be used to customize the application according to the user’s permissions.

A functionality may be associated to users through the admin interface.

Some technical roles are used to define default functionalities depending on the user’s context, they are named anonymous, registered and intranet.

User get functionalities from, by priority order:

If authenticated:

- roles associated to his user profile
- role named `registered`

If his IP is included in the intranet networks:

- role named `intranet`

In all cases:

- role named `anonymous`

Configuration

The vars.yaml file includes variables for managing functionalities.

admin_interface/available_functionalities

List of functionality types that should be available in the administration interface (and added to the functionality table in the database).

For example:

admin_interface:
    available_functionalities:
    - default_basemap
    - print_template
    - mapserver_substitution

The following syntax is also accepted:

admin_interface:
    available_functionalities: [default_basemap, print_template, mapserver_substitution]
functionalities:available_in_templates

Functionalities that are made available to Mako templates (e.g. viewer.js) through the functionality template variable.

For example with:

functionalities:
    available_in_templates:
    - <functionality_name>

if a user is associated to, say, <functionality_name>/value1 and <functionality_name>/value2, then the functionality template variable will be set to a dict with one key/value pair: "<functionality_name>": ["value1","value2"].

Usage in JavaScript client

The functionalities will be sent to the JavaScript client application through the user web service. To be published, a functionality should be present in the parameter functionalities:available_in_templates parameter in the vars.yaml configuration file.

Using Functionalities to configure the basemap to use for each theme

A default basemap may be automatically loaded when the user selects a given theme. This can be configured in the administration interface, as follows: if not available yet, define a default_basemap functionality containing the basemap reference. Edit the theme and select the basemap to load in the default_basemap list. If several default_basemap items are selected, only the first one will be taken into account.

Extend the data model

Note

Extending the data model is not possible in the simple application mode.

The data model can be extended in the file geoportal/<package>_geoportal/models.py.

For example, to add some user details, including a link to a new class named “Title”, add to geoportal/<package>_geoportal/models.py:

from deform.widget import HiddenWidget
from sqlalchemy import Column, ForeignKey, types
from sqlalchemy.orm import backref, relationship

from c2cgeoportal_commons.models.static import User, _schema


class UserDetail(User):
    __tablename__ = 'userdetail'
    __table_args__ = {'schema': _schema}
    __mapper_args__ = {'polymorphic_identity': 'detailed'}
    __colanderalchemy_config__ = {
        'title': _('User detail'),
        'plural': _('User details')
    }
    id = Column(
        types.Integer,
        ForeignKey(_schema + '.user.id'),
        primary_key=True,
        info={
            "colanderalchemy": {
                "missing": None,
                "widget": HiddenWidget()
            }
        }
    )

    phone = Column(
        types.Unicode,
        info={
            'colanderalchemy': {
                'title': _('Phone')
            }
        }
    )

    # title
    title_id = Column(Integer, ForeignKey(_schema + '.title.id'), nullable=False)
    title = relationship("Title", backref=backref('users'), info={
        'colanderalchemy': {
            'title': _('Title')
        }
    })


class Title(Base):
    __tablename__ = 'title'
    __table_args__ = {'schema': _schema}
    __colanderalchemy_config__ = {
        'title': _('Title'),
        'plural': _('Titles')
    }
    id = Column(types.Integer, primary_key=True)
    name = Column(types.Unicode, nullable=False, info={
        'colanderalchemy': {
            'title': _('Name')
        }
    })

Now you need to extend the administration user interface. For this, first ensure that the following files exist (if needed, create them as empty files):

  • geoportal/<package>_geoportal/admin/__init__.py:

  • geoportal/<package>_geoportal/admin/views/__init__.py:

Now, create a file geoportal/<package>_geoportal/admin/views/userdetail.py as follows:

from functools import partial

from <package>_geoportal.models import UserDetail
from c2cgeoform.schema import GeoFormSchemaNode
from c2cgeoform.views.abstract_views import ListField
from deform.widget import FormWidget
from pyramid.view import view_config, view_defaults
from sqlalchemy.orm import aliased, subqueryload

from c2cgeoportal_admin.schemas.roles import roles_schema_node
from c2cgeoportal_admin.views.users import UserViews
from c2cgeoportal_commons.models.main import Role
from c2cgeoportal_commons.models.static import User


_list_field = partial(ListField, UserDetail)

base_schema = GeoFormSchemaNode(UserDetail, widget=FormWidget(fields_template="user_fields"))
base_schema.add(roles_schema_node(User.roles))
base_schema.add_unique_validator(UserDetail.username, UserDetail.id)

settings_role = aliased(Role)


@view_defaults(match_param='table=userdetails')
class UserDetailViews(UserViews):
    _list_fields = [
        _list_field('id'),
        _list_field('username'),
        _list_field('title'),
        _list_field('email'),
        _list_field('last_login'),
        _list_field('expire_on'),
        _list_field('deactivated'),
        _list_field('phone'),
        _list_field(
            "settings_role",
            renderer=lambda user: user.settings_role.name if user.settings_role else "",
            sort_column=settings_role.name,
            filter_column=settings_role.name,
        ),
        _list_field(
            "roles",
            renderer=lambda user: ", ".join([r.name or "" for r in user.roles]),
            filter_column=Role.name,
        ),
    ]
    _id_field = 'id'
    _model = UserDetail
    _base_schema = base_schema

    def _base_query(self):
        return (
            self._request.dbsession.query(UserDetail)
            .distinct()
            .outerjoin(settings_role, settings_role.id == UserDetail.settings_role_id)
            .outerjoin(User.roles)
            .options(subqueryload(User.settings_role))
            .options(subqueryload(User.roles))
        )

    @view_config(
        route_name='c2cgeoform_index',
        renderer='./templates/index.jinja2'
    )
    def index(self):
        return super().index()

    @view_config(
        route_name='c2cgeoform_grid',
        renderer='fast_json'
    )
    def grid(self):
        return super().grid()

    @view_config(
        route_name='c2cgeoform_item',
        request_method='GET',
        renderer='./templates/edit.jinja2'
    )
    def view(self):
        return super().edit()

    @view_config(
        route_name='c2cgeoform_item',
        request_method='POST',
        renderer='./templates/edit.jinja2'
    )
    def save(self):
        return super().save()

    @view_config(
        route_name='c2cgeoform_item',
        request_method='DELETE',
        renderer='fast_json'
    )
    def delete(self):
        return super().delete()

    @view_config(
        route_name='c2cgeoform_item_duplicate',
        request_method='GET',
        renderer='./templates/edit.jinja2'
    )
    def duplicate(self):
        return super().duplicate()

And now the file geoportal/<package>_geoportal/admin/views/title.py:

from functools import partial

from <package>_geoportal.models import Title
from c2cgeoform.schema import GeoFormSchemaNode
from c2cgeoform.views.abstract_views import AbstractViews, ListField
from pyramid.view import view_config, view_defaults


base_schema = GeoFormSchemaNode(Title)
_list_field = partial(ListField, Title)


@view_defaults(match_param='table=titles')
class TitleViews(AbstractViews):
    _list_fields = [
        _list_field('id'),
        _list_field('name'),
    ]
    _id_field = 'id'
    _model = Title
    _base_schema = base_schema

    @view_config(
        route_name='c2cgeoform_index',
        renderer='./templates/index.jinja2'
    )
    def index(self):
        return super().index()

    @view_config(
        route_name='c2cgeoform_grid',
        renderer='fast_json'
    )
    def grid(self):
        return super().grid()

    @view_config(
        route_name='c2cgeoform_item',
        request_method='GET',
        renderer='./templates/edit.jinja2'
    )
    def view(self):
        return super().edit()

    @view_config(
        route_name='c2cgeoform_item',
        request_method='POST',
        renderer='./templates/edit.jinja2'
    )
    def save(self):
        return super().save()

    @view_config(
        route_name='c2cgeoform_item',
        request_method='DELETE',
        renderer='fast_json'
    )
    def delete(self):
        return super().delete()

    @view_config(
        route_name='c2cgeoform_item_duplicate',
        request_method='GET',
        renderer='./templates/edit.jinja2'
    )
    def duplicate(self):
        return super().duplicate()

Change the User page in the admin, add it in your configuration geoportal/vars.yaml:

vars:
    ...
    admin_interface:
        ...
        exclude_pages:
          - users
          - roles
          - functionalities
          - interfaces
        include_pages:
          - url_path: userdetails
            model: <package>_geoportal.models.UserDetail
          - url_path: titles
            model: <package>_geoportal.models.Title
          - url_path: roles
            model: c2cgeoportal_commons.models.main.Role
          - url_path: functionalities
            model: c2cgeoportal_commons.models.main.Functionality
          - url_path: interfaces
            model: c2cgeoportal_commons.models.main.Interface

And finally in geoportal/<package>_geoportal/__init__.py replace config.scan() by:

from c2cgeoportal_admin import PermissionSetter

with PermissionSetter(config):
    # Scan view decorator for adding routes
    config.scan('<package>_geoportal.admin.views')
config.scan(ignore='<package>_geoportal.admin.views')

Build and run the application:

./build <args>
docker compose up -d

Get and run the SQL command to create the tables:

Run Python console:

docker compose exec geoportal python3

Execute the following code:

import sqlalchemy
from c2c.template.config import config

import c2cgeoportal_commons.models

config.init('/etc/geomapfish/config.yaml')
engine = sqlalchemy.engine_from_config(config.get_config(), 'sqlalchemy.')
c2cgeoportal_commons.models.Base.metadata.bind = engine

from <package>_geoportal.models import Title, UserDetail
from sqlalchemy.schema import CreateTable

print(CreateTable(Title.__table__))
print(CreateTable(UserDetail.__table__))

If the generated SQL looks good, do in the same Python console to effectively create the tables:

Title.__table__.create()
UserDetail.__table__.create()