Repository: DOV-Vlaanderen/pydov

Issue:
Join and PropertyInList broken: Query should be an owslib.fes.OgcExpression
<!-- You can ask questions about the DOV webservices or about the `pydov` package. If you have a question about the `pydov` Python package, please use following template. -->

* PyDOV version: 0.3.0
* Python version: 3.6
* Operating System: Windows 10

### Description

The PropertyInList and Join operators are no longer descendents of owslib.fes.Or and therefore fail the pre_search_validation.

We should provide a test for this issue and subsequently fix it by making PropertyInList inherit from owslib.fes.OgcExpression directly.

### What I Did

```
from pydov.search.grondwaterfilter import GrondwaterFilterSearch
from pydov.search.grondwatermonster import GrondwaterMonsterSearch
from pydov.util.query import Join
from owslib.fes import And, PropertyIsEqualTo, PropertyIsLike

gwm = GrondwaterMonsterSearch()
gfs = GrondwaterFilterSearch()

filter_query = And([PropertyIsLike(propertyname='meetnet',
                       literal='meetnet 1 %'),
                    PropertyIsEqualTo(propertyname='gemeente',
                       literal='Kalmthout')])

filters = gfs.search(query=filter_query, return_fields=['pkey_filter'])

samples = gwm.search(query=Join(filters, 'pkey_filter'))
samples.head()
```

```
InvalidSearchParameterError               Traceback (most recent call last)
<ipython-input-1-d2e8840899c9> in <module>()
     14 filters = gfs.search(query=filter_query, return_fields=['pkey_filter'])
     15 
---> 16 samples = gwm.search(query=Join(filters, 'pkey_filter'))
     17 samples.head()

c:\projecten\pydov\pydov_git\pydov\search\grondwatermonster.pyc in search(self, location, query, sort_by, return_fields, max_features)
    140         fts = self._search(location=location, query=query, sort_by=sort_by,
    141                            return_fields=return_fields,
--> 142                            max_features=max_features)
    143 
    144         gw_filters = self._type.from_wfs(fts, self.__wfs_namespace)

c:\projecten\pydov\pydov_git\pydov\search\abstract.pyc in _search(self, location, query, return_fields, sort_by, max_features, extra_wfs_fields)
    605         """
    606         self._pre_search_validation(location, query, sort_by, return_fields,
--> 607                                     max_features)
    608         self._init_namespace()
    609         self._init_wfs()

c:\projecten\pydov\pydov_git\pydov\search\abstract.pyc in _pre_search_validation(self, location, query, sort_by, return_fields, max_features)
    442             if not isinstance(query, owslib.fes.OgcExpression):
    443                 raise InvalidSearchParameterError(
--> 444                     "Query should be an owslib.fes.OgcExpression.")
    445 
    446             filter_request = FilterRequest()

InvalidSearchParameterError: Query should be an owslib.fes.OgcExpression.
```


Discussion:

Patch:
diff --git a/pydov/util/query.py b/pydov/util/query.py
index dae02b5..4baf144 100644
--- a/pydov/util/query.py
+++ b/pydov/util/query.py
@@ -4,10 +4,11 @@
 from owslib.fes import (
     Or,
     PropertyIsEqualTo,
+    OgcExpression,
 )
 
 
-class PropertyInList(object):
+class PropertyInList(OgcExpression):
     """Filter expression to test whether a given property has one of the
     values from a list.
 
@@ -36,6 +37,8 @@ class PropertyInList(object):
             If the given list does not contain at least a single item.
 
         """
+        super(PropertyInList, self).__init__()
+
         if not isinstance(lst, list) and not isinstance(lst, set):
             raise ValueError('list should be of type "list" or "set"')
 


Test patch:
diff --git a/tests/abstract.py b/tests/abstract.py
index 2be19d8..e968722 100644
--- a/tests/abstract.py
+++ b/tests/abstract.py
@@ -30,6 +30,10 @@ from pydov.util.location import (
     Within,
     Box,
 )
+from pydov.util.query import (
+    PropertyInList,
+    Join,
+)
 
 
 def service_ok(timeout=5):
@@ -139,6 +143,17 @@ class AbstractTestSearch(object):
         """
         raise NotImplementedError
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        raise NotImplementedError
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
@@ -582,6 +597,43 @@ class AbstractTestSearch(object):
             query=self.get_valid_query_single(),
             return_fields=self.get_valid_returnfields_extra())
 
+    def test_search_propertyinlist(self, mp_remote_describefeaturetype,
+                                   mp_remote_wfs_feature, mp_dov_xml):
+        """Test the search method with a PropertyInList query.
+
+        Parameters
+        ----------
+        mp_remote_describefeaturetype : pytest.fixture
+            Monkeypatch the call to a remote DescribeFeatureType.
+        mp_remote_wfs_feature : pytest.fixture
+            Monkeypatch the call to get WFS features.
+        mp_dov_xml : pytest.fixture
+            Monkeypatch the call to get the remote XML data.
+
+        """
+        self.get_search_object().search(
+            query=PropertyInList(self.get_wfs_field(), ['a', 'b']))
+
+    def test_search_join(self, mp_remote_describefeaturetype,
+                         mp_remote_wfs_feature, mp_dov_xml):
+        """Test the search method with a Join query.
+
+        Parameters
+        ----------
+        mp_remote_describefeaturetype : pytest.fixture
+            Monkeypatch the call to a remote DescribeFeatureType.
+        mp_remote_wfs_feature : pytest.fixture
+            Monkeypatch the call to get WFS features.
+        mp_dov_xml : pytest.fixture
+            Monkeypatch the call to get the remote XML data.
+
+        """
+        df1 = self.get_search_object().search(
+            query=self.get_valid_query_single())
+
+        df2 = self.get_search_object().search(
+            query=Join(df1, self.get_df_default_columns()[0]))
+
     def test_get_fields_xsd_values(self, mp_remote_xsd):
         """Test the result of get_fields when the XML field has an XSD type.
 
diff --git a/tests/test_search_boring.py b/tests/test_search_boring.py
index f82ffe6..09a4407 100644
--- a/tests/test_search_boring.py
+++ b/tests/test_search_boring.py
@@ -106,6 +106,17 @@ class TestBoringSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'boornummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_grondmonster.py b/tests/test_search_grondmonster.py
index 73a60cb..8fa932a 100644
--- a/tests/test_search_grondmonster.py
+++ b/tests/test_search_grondmonster.py
@@ -79,6 +79,17 @@ class TestGrondmonsterSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'boornummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_grondwaterfilter.py b/tests/test_search_grondwaterfilter.py
index 5926f3c..9f086a9 100644
--- a/tests/test_search_grondwaterfilter.py
+++ b/tests/test_search_grondwaterfilter.py
@@ -80,6 +80,17 @@ class TestGrondwaterfilterSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'filternummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_grondwatermonster.py b/tests/test_search_grondwatermonster.py
index 86aa786..3cbb27c 100644
--- a/tests/test_search_grondwatermonster.py
+++ b/tests/test_search_grondwatermonster.py
@@ -80,6 +80,17 @@ class TestGrondwaterMonsterSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'kationen'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_itp_formelestratigrafie.py b/tests/test_search_itp_formelestratigrafie.py
index 77b5c51..27238b4 100644
--- a/tests/test_search_itp_formelestratigrafie.py
+++ b/tests/test_search_itp_formelestratigrafie.py
@@ -89,6 +89,17 @@ class TestFormeleStratigrafieSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_itp_gecodeerdelithologie.py b/tests/test_search_itp_gecodeerdelithologie.py
index 1d0f1af..3e4d909 100644
--- a/tests/test_search_itp_gecodeerdelithologie.py
+++ b/tests/test_search_itp_gecodeerdelithologie.py
@@ -104,6 +104,17 @@ class TestGecodeerdeLithologieSearch(AbstractTestSearch):
         """
         return 'grondsoort'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_valid_returnfields(self):
         """Get a list of valid return fields from the main type.
 
diff --git a/tests/test_search_itp_geotechnischecodering.py b/tests/test_search_itp_geotechnischecodering.py
index 05146c2..e2df9cc 100644
--- a/tests/test_search_itp_geotechnischecodering.py
+++ b/tests/test_search_itp_geotechnischecodering.py
@@ -93,6 +93,17 @@ class TestGeotechnischeCoderingSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_itp_hydrogeologischestratigrafie.py b/tests/test_search_itp_hydrogeologischestratigrafie.py
index 74845c2..0dcfbae 100644
--- a/tests/test_search_itp_hydrogeologischestratigrafie.py
+++ b/tests/test_search_itp_hydrogeologischestratigrafie.py
@@ -93,6 +93,17 @@ class TestHydrogeologischeStratigrafieSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_itp_informelehydrogeologischestratigrafie.py b/tests/test_search_itp_informelehydrogeologischestratigrafie.py
index 6d6f1fb..7831a09 100644
--- a/tests/test_search_itp_informelehydrogeologischestratigrafie.py
+++ b/tests/test_search_itp_informelehydrogeologischestratigrafie.py
@@ -103,6 +103,17 @@ class TestInformeleHydrogeologischeStratigrafieSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_itp_informelestratigrafie.py b/tests/test_search_itp_informelestratigrafie.py
index c1059d0..7257a6e 100644
--- a/tests/test_search_itp_informelestratigrafie.py
+++ b/tests/test_search_itp_informelestratigrafie.py
@@ -100,6 +100,17 @@ class TestInformeleStratigrafieSearch(AbstractTestSearch):
         """
         return 'diepte_laag_van'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_valid_returnfields(self):
         """Get a list of valid return fields from the main type.
 
diff --git a/tests/test_search_itp_lithologischebeschrijvingen.py b/tests/test_search_itp_lithologischebeschrijvingen.py
index 4541241..50da980 100644
--- a/tests/test_search_itp_lithologischebeschrijvingen.py
+++ b/tests/test_search_itp_lithologischebeschrijvingen.py
@@ -93,6 +93,17 @@ class TestLithologischeBeschrijvingenSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_itp_quartairstratigrafie.py b/tests/test_search_itp_quartairstratigrafie.py
index 43ac3cc..07b5803 100644
--- a/tests/test_search_itp_quartairstratigrafie.py
+++ b/tests/test_search_itp_quartairstratigrafie.py
@@ -88,6 +88,17 @@ class TestQuartairStratigrafieSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'Proefnummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.
 
diff --git a/tests/test_search_sondering.py b/tests/test_search_sondering.py
index 1dd05f2..31a7622 100644
--- a/tests/test_search_sondering.py
+++ b/tests/test_search_sondering.py
@@ -109,6 +109,17 @@ class TestSonderingSearch(AbstractTestSearch):
         """
         return 'onbestaand'
 
+    def get_wfs_field(self):
+        """Get the name of a WFS field.
+
+        Returns
+        -------
+        str
+            The name of the WFS field.
+
+        """
+        return 'sondeernummer'
+
     def get_xml_field(self):
         """Get the name of a field defined in XML only.