# Create and configure: Custom resource page GeoNode allows you to create custom pages that show only a chosen slice of the catalogue. With this approach, you can build a page that has its own title and content, and that displays only dashboards, only resources in a particular category or group, or any other subset that fits your needs. The walkthrough below builds one such page, a "Custom datasets" landing page that lists only datasets. The same recipe works for any other resource type (maps, geostories, documents, dashboards, ...) by changing the `defaultQuery` filter. ## Create the page template Create a new HTML template named `custom_resources_page.html` inside the `templates/` folder of the `my_geonode` project. ```shell touch /opt/geonode-project/my_geonode/src/my_geonode/templates/custom_resources_page.html ``` The project layout after creating the file looks like this: ```text /opt/geonode-project/my_geonode/src/ |-- ... |-- my_geonode/ | |-- ... | +-- templates/ | |-- ... | +-- custom_resources_page.html |-- ... ``` Open the new template and paste in the following content: ```shell vim /opt/geonode-project/my_geonode/src/my_geonode/templates/custom_resources_page.html ``` ```django {% extends "geonode-mapstore-client/resource_page_catalog.html" %} {% load i18n %} {% block content %} {% comment %} The i18n template tag allows to import functionality needed to support translations. It is also possible to access information about the current language in use with: {% get_current_language as LANG %} then the LANG variable can be used inside the template, e.g.:
{{ LANG }}
{% endcomment %}

{% trans "Custom datasets" %}

{% endblock content %} {% block ms_plugins %} msPluginsBlocks = [ { "name": "ResourcesGrid", "cfg": { "id": "catalog", "title": "Custom datasets", "defaultQuery": { "f": "dataset" }, "targetSelector": "#custom-grid", "menuItems": [] } }, { "name": "ResourcesFiltersForm", "cfg": { "fields": getPageFilterForm() } } ]; {% endblock ms_plugins %} ``` A few things to notice in the template: - It extends `geonode-mapstore-client/resource_page_catalog.html`, the base template that `geonode-mapstore-client` ships for catalogue style pages. - The `content` block defines the visible layout: a heading and an empty `
` that the MapStore `ResourcesGrid` plugin fills in. - The `ms_plugins` block declares which MapStore plugins are mounted on the page. `ResourcesGrid` is bound to `#custom-grid` and filtered by `defaultQuery.f = "dataset"`, so only datasets are listed. `ResourcesFiltersForm` adds the filters sidebar. ## Wire the page into urls.py Add the new page to the `urlpatterns` list of the project's `urls.py`. ```shell vim /opt/geonode-project/my_geonode/src/my_geonode/urls.py ``` ```python from django.views.generic import TemplateView from django.conf.urls import url urlpatterns += [ url('custom_resources_page', view=TemplateView.as_view(template_name='custom_resources_page.html')) ] ``` Restart the Django process so that the new URL is picked up. If you are running in development mode, the autoreloader does this for you. Now open the browser and navigate to `http://localhost:8000/custom_resources_page`. You should see a new page rendering a grid of datasets from the database, with the filters sidebar on the side. ## Customize for other resource types To target a different resource type, change the `defaultQuery.f` value in the `ResourcesGrid` configuration. Examples: | Resource type | `defaultQuery.f` value | | --- | --- | | Datasets | `dataset` | | Documents | `document` | | Maps | `map` | | GeoStories | `geostory` | | Dashboards | `dashboard` | You can also combine filters (for example by category, group, or owner) by adding more keys to `defaultQuery`. The keys mirror the query parameters accepted by the GeoNode resources API. ## Pass data from the backend (views.py) to the page So far the URL has been wired straight to `TemplateView`, which renders the template with no extra data. When a page needs values that have to be computed on the server (summary counts shown next to the title, the active user's profile, a feature flag, and so on), replace `TemplateView` with a subclass that overrides `get_context_data` and exposes those values through the template *context*. ### Write a CustomPageView in views.py If the project doesn't already have a `views.py`, create one inside the Django app folder. ```shell touch /opt/geonode-project/my_geonode/src/my_geonode/views.py vim /opt/geonode-project/my_geonode/src/my_geonode/views.py ``` The example below upgrades the Custom datasets page from the previous sections so it computes the dataset counts that correspond to the *My resources*, *Favorites* and *Featured* filter chips offered by MapStore's `ResourcesGrid`, and exposes them as `custom_counts` in the template context. ```python from django.db.models import Subquery from django.views.generic import TemplateView from geonode.favorite.models import Favorite from geonode.security.utils import get_resources_with_perms DATASET_RESOURCE_TYPE = "dataset" class CustomPageView(TemplateView): """Renders the Custom datasets catalogue page. Adds the dataset counts for the My resources, Favorites and Featured filter chips to the template context, so the same numbers shown in the ResourcesGrid filters can also be displayed inline next to the page title. """ template_name = "custom_resources_page.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user datasets = get_resources_with_perms(user).filter(resource_type=DATASET_RESOURCE_TYPE) featured_count = datasets.filter(featured=True).count() if user.is_authenticated: my_resources_count = datasets.filter(owner=user).count() favorites_count = datasets.filter( pk__in=Subquery( Favorite.objects.filter( user=user, content_type__model=DATASET_RESOURCE_TYPE, ).values_list("object_id", flat=True) ) ).count() else: # The matching filter chips in the sidebar are disabled for # anonymous users, so report zero rather than running queries # scoped to a user that isn't there. my_resources_count = 0 favorites_count = 0 context["custom_counts"] = { "my_resources": my_resources_count, "favorites": favorites_count, "featured": featured_count, } return context ``` Things worth pointing out in this example: - `get_resources_with_perms(user)` is the GeoNode helper that returns only the resources the requesting user is allowed to see. It already takes care of permissions and anonymous access, so always start from it instead of querying the model directly. That way the counts you compute will match what MapStore actually shows in the grid. - The `featured` flag is global, so its count is always computed. The *My resources* and *Favorites* counts are guarded by `user.is_authenticated`, because the matching filter chips in the sidebar are disabled for anonymous users anyway. - Everything goes into one dictionary key (`custom_counts`), so the template only has to deal with a single namespace. - If you adapt this view for another page, the only project specific bits are `DATASET_RESOURCE_TYPE`, the `featured` filter (only datasets and maps carry that flag) and the dictionary key itself. Everything else, `get_resources_with_perms`, the `Subquery(Favorite.objects.…)` block, the `get_context_data` plumbing, can be reused as is. ### Point urls.py at CustomPageView Replace the plain `TemplateView.as_view(...)` registration created in [Wire the page into urls.py](#wire-the-page-into-urlspy) with the new view class. Keep the same URL path so that any existing links keep working. ```shell vim /opt/geonode-project/my_geonode/src/my_geonode/urls.py ``` ```python from django.urls import re_path from .views import CustomPageView urlpatterns += [ re_path(r"^custom_resources_page$", CustomPageView.as_view(), name="custom_resources_page"), ] ``` The `name="custom_resources_page"` argument makes the URL reverseable elsewhere in the project via `{% url 'custom_resources_page' %}` or `reverse("custom_resources_page")`. ### Render the context in the template Inside the template referenced by `template_name`, every key returned from `get_context_data` is available as an ordinary Django variable. Open the same `custom_resources_page.html` you created earlier, add a counts list under the title and drive the grid title from the context as well: ```shell vim /opt/geonode-project/my_geonode/src/my_geonode/templates/custom_resources_page.html ``` ```diff {% extends "geonode-mapstore-client/resource_page_catalog.html" %} {% load i18n %} {% block content %}

{% trans "Custom datasets" %}

+
    +
  • {% trans "My resources" %}: {{ custom_counts.my_resources }}
  • +
  • {% trans "Favorites" %}: {{ custom_counts.favorites }}
  • +
  • {% trans "Featured" %}: {{ custom_counts.featured }}
  • +
{% endblock content %} {% block ms_plugins %} msPluginsBlocks = [ { "name": "ResourcesGrid", "cfg": { "id": "catalog", "title": "Custom datasets", "defaultQuery": { "f": "dataset" }, "targetSelector": "#custom-grid", "menuItems": [] } }, { "name": "ResourcesFiltersForm", "cfg": { "fields": getPageFilterForm() } } ]; {% endblock ms_plugins %} ``` A few things to keep in mind: - **Reading the data is just `{{ ... }}`.** In the view we put a Python dictionary into `custom_counts`. In the template, you ask for one of its keys with `{{ custom_counts.featured }}`. The dot works for both attributes and dictionary keys, so `dict["featured"]` in Python becomes `dict.featured` here. No extra setup is needed. - **You can use the same variables inside the `ms_plugins` block.** That block is plain JavaScript that Django renders just like the HTML, so a tag like `{{ custom_counts.featured }}` works in there too. That's how we baked the featured count into the grid title above. If you ever need to hand a larger chunk of data to the JavaScript (a list, a nested dict, etc.), don't try to write it inline. Use the built-in `json_script` filter: ```django {{ custom_counts|json_script:"custom-counts-data" }} ``` Then read it from JavaScript: ```js const counts = JSON.parse(document.getElementById("custom-counts-data").textContent); ``` This is safer (Django escapes everything for you) and keeps the markup readable. - **The view runs once per page load, not on every click.** Context values are great for things that are decided when the page is first opened: who the current user is, what language they prefer, whether a feature is enabled. They are *not* good for things that change while the user clicks around. If you want numbers that update live (for example, when the user toggles a filter), let MapStore's `ResourcesGrid` fetch them from the GeoNode resources API instead of putting them into the context. #### [Next Section: Configure a new plugin extension](004_EXTENSION.md)