From b329ffc922d6ee35152c8401c22c82dd6a5aefab Mon Sep 17 00:00:00 2001
From: Tor-Einar Skog <tor-einar.skog@nibio.no>
Date: Wed, 28 Aug 2024 14:32:40 +0200
Subject: [PATCH] First version of admin page for weather station data sources

---
 .../WeatherStationDataSourceController.java   | 125 +++++++++++++++++-
 .../session/PointOfInterestBean.java          |  35 +++++
 .../entity/WeatherStationDataSource.java      |   2 +
 .../vips/logic/i18n/vipslogictexts.properties |   6 +
 .../logic/i18n/vipslogictexts_bs.properties   |   6 +
 .../logic/i18n/vipslogictexts_hr.properties   |   6 +
 .../logic/i18n/vipslogictexts_nb.properties   |   6 +
 .../logic/i18n/vipslogictexts_sr.properties   |   6 +
 .../i18n/vipslogictexts_zh_CN.properties      |   6 +
 .../weatherStationDataSourceForm.json         |  59 +++++++++
 .../weatherStationDataSourceForm.ftl          |  97 ++++++++++++++
 .../weatherStationDataSourceList.ftl          |   4 +-
 12 files changed, 355 insertions(+), 3 deletions(-)
 create mode 100644 src/main/webapp/formdefinitions/weatherStationDataSourceForm.json
 create mode 100644 src/main/webapp/templates/weatherStationDataSourceForm.ftl

diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/WeatherStationDataSourceController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/WeatherStationDataSourceController.java
index 3efd4961..766d208f 100644
--- a/src/main/java/no/nibio/vips/logic/controller/servlet/WeatherStationDataSourceController.java
+++ b/src/main/java/no/nibio/vips/logic/controller/servlet/WeatherStationDataSourceController.java
@@ -27,9 +27,19 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import no.nibio.vips.gis.LonLatStringFormatException;
 import no.nibio.vips.logic.controller.session.PointOfInterestBean;
 import no.nibio.vips.logic.entity.VipsLogicUser;
-import no.nibio.vips.logic.entity.WeatherStationDataSource;;
+import no.nibio.vips.logic.entity.WeatherStationDataSource;
+import no.nibio.vips.logic.util.Globals;
+import no.nibio.vips.logic.util.SystemTime;
+import no.nibio.vips.util.ServletUtil;
+import no.nibio.web.forms.FormValidation;
+import no.nibio.web.forms.FormValidationException;
+import no.nibio.web.forms.FormValidator;;
 
 /**
  * Handles actions regarding listing and modifying weather station data sources
@@ -39,6 +49,8 @@ import no.nibio.vips.logic.entity.WeatherStationDataSource;;
  */
 public class WeatherStationDataSourceController  extends HttpServlet{
 
+    private static Logger LOGGER = LoggerFactory.getLogger(WeatherStationDataSourceController.class);
+
     @EJB
     PointOfInterestBean poiBean;
 
@@ -65,9 +77,120 @@ public class WeatherStationDataSourceController  extends HttpServlet{
         if(action == null)
         {
             List<WeatherStationDataSource> wsDataSources = poiBean.getWeatherStationDataSources();
+            request.setAttribute("messageKey", request.getParameter("messageKey") != null ? request.getParameter("messageKey") : null);
             request.setAttribute("weatherStationDataSources", wsDataSources);
             request.getRequestDispatcher("/weatherStationDataSourceList.ftl").forward(request, response);
         }
+        else if(action.equals("editWeatherStationDataSource") || action.equals("newWeatherStationDataSource"))
+        {
+            WeatherStationDataSource weatherStationDataSource = null;
+            try 
+            { 
+                weatherStationDataSource = poiBean.getWeatherStationDataSource(Integer.valueOf(request.getParameter("weatherStationDataSourceId")));
+            }
+            catch(NullPointerException | NumberFormatException ex){}
+
+            if(weatherStationDataSource == null)
+            {
+                LOGGER.debug("Could not find weather data source with ID = " + request.getParameter("weatherStationDataSourceId"));
+                weatherStationDataSource = new WeatherStationDataSource();
+            }
+            
+            request.setAttribute("messageKey", request.getParameter("messageKey") != null ? request.getParameter("messageKey") : null);
+            request.setAttribute("weatherStationDataSource", weatherStationDataSource);
+            request.getRequestDispatcher("/weatherStationDataSourceForm.ftl").forward(request, response);
+        }
+        else if(action.equals("weatherStationDataSourceFormSubmit"))
+        {
+            try
+            {
+                // Check that the provided id is either -1 or it already exists
+                Integer weatherStationDataSourceId = null;
+                WeatherStationDataSource weatherStationDataSource;
+                try
+                {
+                    weatherStationDataSourceId = Integer.valueOf(request.getParameter("weatherStationDataSourceId"));
+                }
+                catch(NullPointerException | NumberFormatException ex)
+                {
+                    response.sendError(400,"Wrong format of id for weather station data source");
+                }
+                if(weatherStationDataSourceId < 0)
+                {
+                    weatherStationDataSource = new WeatherStationDataSource();
+                }
+                else 
+                {
+                    weatherStationDataSource = poiBean.getWeatherStationDataSource(weatherStationDataSourceId);
+                    if(weatherStationDataSource == null)
+                    {
+                        response.sendError(404,"Could not find weather station data source with id=" + weatherStationDataSourceId);
+                    }
+                }
+                FormValidation formValidation = FormValidator.validateForm("weatherStationDataSourceForm", request, getServletContext());
+                if(formValidation.isValid())
+                {
+                    weatherStationDataSource.setName(formValidation.getFormField("name").getWebValue());
+                    weatherStationDataSource.setDefaultDescription(formValidation.getFormField("defaultDescription").getWebValue());
+                    weatherStationDataSource.setUri(formValidation.getFormField("uri").getWebValue());
+                    weatherStationDataSource.setDatafetchUriExpression(formValidation.getFormField("datafetchUriExpression").getWebValue());
+                    weatherStationDataSource.setInfoUriExpression(formValidation.getFormField("infoUriExpression").getWebValue());
+                    weatherStationDataSource.setIsGrid(formValidation.getFormField("isGrid").getWebValue() != null);
+                    weatherStationDataSource = poiBean.storeWeatherStationDataSource(weatherStationDataSource);
+
+                    response.sendRedirect(
+                            Globals.PROTOCOL + "://" + ServletUtil.getServerName(request)
+                            + "/weatherstationdatasource?action=editWeatherStationDataSource&weatherStationDataSourceId=" + weatherStationDataSource.getWeatherStationDataSourceId()
+                            + "&messageKey=weatherStationDataSourceStored" 
+                    );
+                }
+                else
+                {
+                    request.setAttribute("formValidation", formValidation);
+                    request.setAttribute("weatherStationDataSource", weatherStationDataSource);
+                    LOGGER.debug("Form NOT valid. Dispatching");
+                    request.getRequestDispatcher("/weatherStationDataSourceForm.ftl").forward(request, response);
+                }
+            }
+            catch(FormValidationException ex)
+            {
+                ex.printStackTrace();
+                response.sendError(500, ex.getClass().toString() + ": " + ex.getMessage());
+            }
+        }
+        else if(action.equals("deleteWeatherStationDataSource"))
+        {
+
+            try
+            {
+                Integer weatherStationDataSourceId = Integer.valueOf(request.getParameter("weatherStationDataSourceId"));
+                WeatherStationDataSource weatherStationDataSource = poiBean.getWeatherStationDataSource(weatherStationDataSourceId);
+                if(weatherStationDataSource == null)
+                {
+                    response.sendError(404,"Could not find weather station data source with id=" + weatherStationDataSourceId);
+                }
+                // Check: Can it be deleted? Not if organizations or weatherStations refer to it
+                if(poiBean.isweatherStationDataSourceDeleteable(weatherStationDataSource))
+                {
+                    poiBean.deleteWeatherStationDataSource(weatherStationDataSource);
+                    // Redirect to list from which the deleted item has been removed
+                    response.sendRedirect(
+                            Globals.PROTOCOL + "://" + ServletUtil.getServerName(request)
+                            + "/weatherstationdatasource?messageKey=weatherStationDataSourceDeleted"
+                    );
+                }
+                else 
+                {
+                    response.sendError(403,weatherStationDataSource.getName() + " is in use by one or more organizations, so it cannot be deleted. Hit the browser's back button return to your form.");
+                }
+            }
+            catch(NullPointerException | NumberFormatException ex)
+            {
+                response.sendError(400,"Wrong format of id for weather station data source");
+            }
+
+            
+        }
     }
 
     // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
diff --git a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java
index b7f95ffa..22229cf8 100755
--- a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java
+++ b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java
@@ -276,6 +276,41 @@ public class PointOfInterestBean {
         return em.createNamedQuery("WeatherStationDataSource.findGridSources").getResultList();
     }
 
+    /**
+     * Checks if the weather station data source can be deleted from the system. Criteria:
+     * <ul>
+     * <li>Not referenced from public.point_of_interest_weather_station</li>
+     * <li>Not referenced from public.organization</li>
+     * </ul> 
+     * @param weatherStationDataSource
+     * @return
+     */
+    public Boolean isweatherStationDataSourceDeleteable(WeatherStationDataSource weatherStationDataSource)
+    {
+        Query poiRefQuery = em.createQuery("SELECT COUNT(*) FROM PointOfInterestWeatherStation poiws where poiws.weatherStationDataSourceId = :weatherStationDataSourceId");
+        Long weatherStationReferences = (Long) poiRefQuery.setParameter("weatherStationDataSourceId", weatherStationDataSource).getSingleResult();
+        if(weatherStationReferences > 0)
+        {
+            return false;
+        }
+
+        Query orgRefQuery = em.createQuery("SELECT COUNT(*) FROM Organization o where o.defaultGridWeatherStationDataSource = :weatherStationDataSourceId");
+        Long organizationReferences = (Long) orgRefQuery.setParameter("weatherStationDataSourceId", weatherStationDataSource).getSingleResult();
+        return organizationReferences == 0;
+    }
+
+    public void deleteWeatherStationDataSource(WeatherStationDataSource weatherStationDataSource)
+    {
+        WeatherStationDataSource sourceToDelete = em.find(WeatherStationDataSource.class, weatherStationDataSource.getWeatherStationDataSourceId());
+        em.remove(sourceToDelete);
+    }
+
+    public WeatherStationDataSource storeWeatherStationDataSource(WeatherStationDataSource weatherStationDataSource)
+    {
+        weatherStationDataSource = em.merge(weatherStationDataSource);
+        return weatherStationDataSource;
+    }
+
     public PointOfInterestWeatherStation storeWeatherStation(PointOfInterestWeatherStation weatherStation) {
         weatherStation = em.merge(weatherStation);
         return weatherStation;
diff --git a/src/main/java/no/nibio/vips/logic/entity/WeatherStationDataSource.java b/src/main/java/no/nibio/vips/logic/entity/WeatherStationDataSource.java
index de26e921..b31bd3aa 100755
--- a/src/main/java/no/nibio/vips/logic/entity/WeatherStationDataSource.java
+++ b/src/main/java/no/nibio/vips/logic/entity/WeatherStationDataSource.java
@@ -72,6 +72,8 @@ public class WeatherStationDataSource implements Serializable {
     @Column(name = "is_grid")
     private Boolean isGrid;
 
+   
+
     public Boolean getIsGrid() {
         return isGrid;
     }
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
index 3c388d81..8eaade79 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
@@ -1057,3 +1057,9 @@ doNotUse=Do not use
 defaultGridWeatherStationDataSource=Gridded weather data source
 weatherStationDataSources=Weather station data sources
 newWeatherStationDataSource=New weather (station) data source
+editWeatherStationDataSource=Edit weather (station) data source
+datafetchUriExpression=URI template for requesting data
+infoUriExpression=Template for request for station information
+isGridWeatherDataSource=This is a grid based weather data source
+weatherStationDataSourceStored=Weather (station) data source was successfully stored
+weatherStationDataSourceDeleted=The weather (station) data source was successfully deleted
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties
index 5911156f..69fb5f1d 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties
@@ -1051,3 +1051,9 @@ doNotUse=Do not use
 defaultGridWeatherStationDataSource=Gridded weather data source
 weatherStationDataSources=Weather station data sources
 newWeatherStationDataSource=New weather (station) data source
+editWeatherStationDataSource=Edit weather (station) data source
+datafetchUriExpression=URI template for requesting data
+infoUriExpression=Template for request for station information
+isGridWeatherDataSource=This is a grid based weather data source
+weatherStationDataSourceStored=Weather (station) data source was successfully stored
+weatherStationDataSourceDeleted=The weather (station) data source was successfully deleted
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties
index 00d9452c..fcde6d89 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties
@@ -1049,3 +1049,9 @@ doNotUse=Do not use
 defaultGridWeatherStationDataSource=Gridded weather data source
 weatherStationDataSources=Weather station data sources
 newWeatherStationDataSource=New weather (station) data source
+editWeatherStationDataSource=Edit weather (station) data source
+datafetchUriExpression=URI template for requesting data
+infoUriExpression=Template for request for station information
+isGridWeatherDataSource=This is a grid based weather data source
+weatherStationDataSourceStored=Weather (station) data source was successfully stored
+weatherStationDataSourceDeleted=The weather (station) data source was successfully deleted
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
index 5a472851..c38d45b8 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
@@ -1057,3 +1057,9 @@ doNotUse=Ikke bruk
 defaultGridWeatherStationDataSource=GRID-basert v\u00e6rdatakilde
 weatherStationDataSources=V\u00e6r(stasjons)datakilder
 newWeatherStationDataSource=Ny v\u00e6r(stasjons)datakilde
+editWeatherStationDataSource=Rediger v\u00e6r(stasjons)datakilde
+datafetchUriExpression=URI-mal for v\u00e6rdataforesp\u00f8rsel
+infoUriExpression=Mal for henvendelse om stasjonsinformasjon
+isGridWeatherDataSource=Er en grid-basert v\u00e6rdatakilde
+weatherStationDataSourceStored=V\u00e6r(stasjons)datakilden ble lagret
+weatherStationDataSourceDeleted=V\u00e6r(stasjons)datakilden ble slettet
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties
index c3a70f06..7eb7473a 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties
@@ -1051,3 +1051,9 @@ doNotUse=Do not use
 defaultGridWeatherStationDataSource=Gridded weather data source
 weatherStationDataSources=Weather station data sources
 newWeatherStationDataSource=New weather (station) data source
+editWeatherStationDataSource=Edit weather (station) data source
+datafetchUriExpression=URI template for requesting data
+infoUriExpression=Template for request for station information
+isGridWeatherDataSource=This is a grid based weather data source
+weatherStationDataSourceStored=Weather (station) data source was successfully stored
+weatherStationDataSourceDeleted=The weather (station) data source was successfully deleted
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties
index 47043bc4..8e0f5706 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties
@@ -1045,3 +1045,9 @@ doNotUse=Do not use
 defaultGridWeatherStationDataSource=Gridded weather data source
 weatherStationDataSources=Weather station data sources
 newWeatherStationDataSource=New weather (station) data source
+editWeatherStationDataSource=Edit weather (station) data source
+datafetchUriExpression=URI template for requesting data
+infoUriExpression=Template for request for station information
+isGridWeatherDataSource=This is a grid based weather data source
+weatherStationDataSourceStored=Weather (station) data source was successfully stored
+weatherStationDataSourceDeleted=The weather (station) data source was successfully deleted
diff --git a/src/main/webapp/formdefinitions/weatherStationDataSourceForm.json b/src/main/webapp/formdefinitions/weatherStationDataSourceForm.json
new file mode 100644
index 00000000..35cf4803
--- /dev/null
+++ b/src/main/webapp/formdefinitions/weatherStationDataSourceForm.json
@@ -0,0 +1,59 @@
+{
+    "_licenseNote": [
+       "Copyright (c) 2024 NIBIO <http://www.nibio.no/>. ",
+        "",
+        "This file is part of VIPSLogic. ",
+        
+        "This program is free software: you can redistribute it and/or modify",
+        "it under the terms of the GNU Affero General Public License as published by",
+        "the Free Software Foundation, either version 3 of the License, or",
+        "(at your option) any later version.",
+        
+        "This program is distributed in the hope that it will be useful,",
+        "but WITHOUT ANY WARRANTY; without even the implied warranty of",
+        "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the",
+        "GNU Affero General Public License for more details.",
+ 
+        "You should have received a copy of the GNU Affero General Public License",
+        "along with this program.  If not, see <https://www.gnu.org/licenses/>."
+    ],
+    "_comment" : "Structure of the organizationForm and how to validate it",
+    "fields": [
+        {
+            "name" : "weatherStationDataSourceId",
+            "dataType" : "INTEGER",
+            "required" : true
+        },
+        {
+            "name" : "name",
+            "dataType" : "STRING",
+            "required" : true
+        },
+        {
+            "name" : "defaultDescription",
+            "dataType" : "STRING",
+            "required" : false
+        },
+        {
+            "name" : "uri",
+            "dataType" : "STRING",
+            "required" : false
+        },
+        {
+            "name" : "datafetchUriExpression",
+            "dataType" : "STRING",
+            "required" : true
+        },
+        {
+            "name" : "infoUriExpression",
+            "dataType" : "STRING",
+            "required" : false
+        },
+        {
+            "name" : "isGrid",
+            "dataType" : "STRING",
+            "required" : false
+        }
+        
+    ]
+}
diff --git a/src/main/webapp/templates/weatherStationDataSourceForm.ftl b/src/main/webapp/templates/weatherStationDataSourceForm.ftl
new file mode 100644
index 00000000..d386d502
--- /dev/null
+++ b/src/main/webapp/templates/weatherStationDataSourceForm.ftl
@@ -0,0 +1,97 @@
+<#-- 
+  Copyright (c) 2024 NIBIO <http://www.nibio.no/>. 
+  
+  This file is part of VIPSLogic.
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU Affero General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU Affero General Public License for more details.
+
+  You should have received a copy of the GNU Affero General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+--><#include "master.ftl">
+<#assign formId = "weatherStationDataSourceForm">
+<#macro page_head>
+        <title><#if weatherStationDataSource.weatherStationDataSourceId?has_content>${i18nBundle.editWeatherStationDataSource} ${weatherStationDataSource.name}<#else>${i18nBundle.newWeatherStationDataSource}</#if></title>
+</#macro>
+<#macro custom_css>
+	<link rel="stylesheet" type="text/css" href="/css/3rdparty/ol.css"/ >
+</#macro>
+<#macro custom_js>
+	<script type="text/javascript" src="/js/constants.js"></script>
+	<script type="text/javascript" src="/js/resourcebundle.js"></script>
+	<script type="text/javascript" src="/js/validateForm.js"></script>
+	<script type="text/javascript">
+		$(document).ready(function() {
+			// Load main form definition (for validation)
+			loadFormDefinition("${formId}");
+			
+		});
+        function handleWeatherStationDataSourceDelete(weatherStationDataSourceId){  
+                window.location.href="/weatherstationdatasource?action=deleteWeatherStationDataSource&weatherStationDataSourceId=" + weatherStationDataSourceId;
+        }
+                
+	</script>
+</#macro>
+<#macro page_contents>
+<div class="singleBlockContainer">
+	<p><a href="/weatherstationdatasource" class="btn btn-default back" role="button">${i18nBundle.back}</a></p>
+    <h1><#if weatherStationDataSource.weatherStationDataSourceId?has_content>${i18nBundle.editWeatherStationDataSource} ${weatherStationDataSource.name}<#else>${i18nBundle.newWeatherStationDataSource}</#if></h1>
+    <div id="errorMsgEl" class="alert alert-danger" <#if !formValidation?has_content> style="display:none;"</#if>>
+		<#if formValidation?has_content>${formValidation.validationMessages?replace("\n", "<br>")}</#if>
+	</div>
+    <#if messageKey?has_content>
+		<div class="alert alert-success">${i18nBundle(messageKey)}</div>
+	</#if>
+    <div class="row">
+		<div class="col-md-12">
+			<form id="${formId}" role="form" action="/weatherstationdatasource?action=weatherStationDataSourceFormSubmit" method="POST" onsubmit="return validateForm(this);">
+                <input type="hidden" name="weatherStationDataSourceId" value="${weatherStationDataSource.weatherStationDataSourceId!"-1"}"/>
+                <div class="form-group">
+                    <label for="name">${i18nBundle.name}</label>
+                    <input type="text" class="form-control" name="name" placeholder="${i18nBundle.name}" value="${(weatherStationDataSource.name)!""}" onblur="validateField(this); "/>
+			        <span class="help-block" id="${formId}_name_validation"></span>
+			    </div>
+                <div class="form-group">
+                    <label for="defaultDescription">${i18nBundle.description}</label>
+                    <input type="text" class="form-control" name="defaultDescription" placeholder="${i18nBundle.description}" value="${(weatherStationDataSource.defaultDescription)!""}" onblur="validateField(this); "/>
+			        <span class="help-block" id="${formId}_defaultDescription_validation"></span>
+			    </div>
+                <div class="form-group">
+                    <label for="uri">URI</label>
+                    <input type="url" class="form-control" name="uri" placeholder="URI" value="${(weatherStationDataSource.uri)!""}" onblur="validateField(this); "/>
+			        <span class="help-block" id="${formId}_uri_validation"></span>
+			    </div>
+                <div class="form-group">
+                    <label for="datafetchUriExpression">${i18nBundle.datafetchUriExpression}</label>
+                    <input type="url" class="form-control" name="datafetchUriExpression" placeholder="${i18nBundle.datafetchUriExpression}" value="${(weatherStationDataSource.datafetchUriExpression)!""}" onblur="validateField(this); "/>
+			        <span class="help-block" id="${formId}_datafetchUriExpression_validation"></span>
+			    </div>
+                <div class="form-group">
+                    <label for="infoUriExpression">${i18nBundle.infoUriExpression}</label>
+                    <input type="url" class="form-control" name="infoUriExpression" placeholder="${i18nBundle.infoUriExpression}" value="${(weatherStationDataSource.infoUriExpression)!""}" onblur="validateField(this); "/>
+			        <span class="help-block" id="${formId}_infoUriExpression_validation"></span>
+			    </div>
+                <div class="form-group">
+                <div class="checkbox">
+                    <label>
+                    <input type="checkbox" id="isGrid" name="isGrid" value="true"<#if weatherStationDataSource.isGrid?has_content && weatherStationDataSource.isGrid == true> checked="checked"</#if>/>
+                    </label>
+                    ${i18nBundle.isGridWeatherDataSource}
+                    <span class="help-block" id="${formId}_isGrid_validation"></span>
+                </div>
+                <button type="submit" class="btn btn-default">${i18nBundle.submit}</button>
+			  <#if weatherStationDataSource.weatherStationDataSourceId?has_content>
+			  <button type="button" class="btn btn-danger" onclick="if(confirm('${i18nBundle.confirmDelete}')){handleWeatherStationDataSourceDelete(${weatherStationDataSource.weatherStationDataSourceId});}">${i18nBundle.delete}</button>
+			  </#if>
+            </form>
+        </div>
+    </div>
+</div>
+</#macro>
+<@page_html/>
\ No newline at end of file
diff --git a/src/main/webapp/templates/weatherStationDataSourceList.ftl b/src/main/webapp/templates/weatherStationDataSourceList.ftl
index 3645a22c..82c6b0eb 100644
--- a/src/main/webapp/templates/weatherStationDataSourceList.ftl
+++ b/src/main/webapp/templates/weatherStationDataSourceList.ftl
@@ -24,16 +24,16 @@
 </#macro>
 <#macro page_contents>
 <div class="singleBlockContainer">
-<h1>${i18nBundle.weatherStationDataSources}</h1>
 <#if messageKey?has_content>
         <div class="alert alert-success">${i18nBundle(messageKey)}</div>
 </#if>
+<h1>${i18nBundle.weatherStationDataSources}</h1>
 <ul>
 <#list weatherStationDataSources as dSource>
 <li><a href="/weatherstationdatasource?action=editWeatherStationDataSource&weatherStationDataSourceId=${dSource.weatherStationDataSourceId}">${dSource.name}</a></li>
 </#list>
 </ul>
-<p><a href="/weatherstationdatasource??action=newWeatherStationDataSource" class="btn btn-default back" role="button">${i18nBundle.newWeatherStationDataSource}</a></p>
+<p><a href="/weatherstationdatasource?action=newWeatherStationDataSource" class="btn btn-default back" role="button">${i18nBundle.newWeatherStationDataSource}</a></p>
 </div>
 </#macro>
 <@page_html/>
-- 
GitLab