Commit 3a615383 authored by Florent Chehab's avatar Florent Chehab

feat(standard filtering, tweaks): REST Api and other

* Filtering on client request should now be performed with the standard `?attr=...` syntaxe
* Frontend updated for this new syntaxe
* Backend and frontend documentation updated with new changes
* Updated the location of the the api documentation to `/api-doc`
* Fixed bug preventing api-doc to render
* backend python requirements updated
* Updated dockerfile / docker-compose to make sure we wait for the db

Fixes #97 #80
parent 243f43bf
Pipeline #37640 passed with stages
in 4 minutes and 23 seconds
...@@ -21,3 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ...@@ -21,3 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN pip install --upgrade pip RUN pip install --upgrade pip
COPY requirements.txt /usr/src/app/requirements.txt COPY requirements.txt /usr/src/app/requirements.txt
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
## Add the wait script to the image to wait for the database to be up for sure
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.5.0/wait /wait
RUN chmod +x /wait
...@@ -359,6 +359,12 @@ class EssentialModuleViewSet(BaseModelViewSet): ...@@ -359,6 +359,12 @@ class EssentialModuleViewSet(BaseModelViewSet):
This allows to not do this query for all elements and improves This allows to not do this query for all elements and improves
performances. You can look at the comment below for more information. performances. You can look at the comment below for more information.
""" """
# When generating the API documentation (url: /api-doc) the request would be None
# and we don't need to do anything special
if self.request is None:
return super().get_serializer_context()
fake_edit_request = FakeRequest(self.request.user, "PUT") fake_edit_request = FakeRequest(self.request.user, "PUT")
user_can_edit = True user_can_edit = True
......
...@@ -26,8 +26,5 @@ class CampusTaggedItemSerializer(TaggedItemSerializer): ...@@ -26,8 +26,5 @@ class CampusTaggedItemSerializer(TaggedItemSerializer):
class CampusTaggedItemViewSet(TaggedItemViewSet): class CampusTaggedItemViewSet(TaggedItemViewSet):
queryset = CampusTaggedItem.objects.all() # pylint: disable=E1101 queryset = CampusTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CampusTaggedItemSerializer serializer_class = CampusTaggedItemSerializer
end_point_route = r"campusTaggedItems/(?P<campus_id>[0-9]+)" end_point_route = "campusTaggedItems"
filterset_fields = ("campus",)
def get_queryset(self):
campus_id = self.kwargs["campus_id"]
return super().get_queryset().filter(campus=campus_id).distinct()
...@@ -26,8 +26,5 @@ class CityTaggedItemSerializer(TaggedItemSerializer): ...@@ -26,8 +26,5 @@ class CityTaggedItemSerializer(TaggedItemSerializer):
class CityTaggedItemViewSet(TaggedItemViewSet): class CityTaggedItemViewSet(TaggedItemViewSet):
queryset = CityTaggedItem.objects.all() # pylint: disable=E1101 queryset = CityTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CityTaggedItemSerializer serializer_class = CityTaggedItemSerializer
end_point_route = r"cityTaggedItems/(?P<city_id>[0-9]+)" end_point_route = "cityTaggedItems"
filterset_fields = ("city",)
def get_queryset(self):
city_id = self.kwargs["city_id"]
return super().get_queryset().filter(city=city_id).distinct()
...@@ -19,8 +19,5 @@ class CountryDriViewSet(ModuleViewSet): ...@@ -19,8 +19,5 @@ class CountryDriViewSet(ModuleViewSet):
queryset = CountryDri.objects.all() # pylint: disable=E1101 queryset = CountryDri.objects.all() # pylint: disable=E1101
serializer_class = CountryDriSerializer serializer_class = CountryDriSerializer
permission_classes = (IsStaff | IsDri | NoPost,) permission_classes = (IsStaff | IsDri | NoPost,)
end_point_route = r"countryDri/(?P<country_id>[a-zA-Z]+)" end_point_route = "countryDri"
filterset_fields = ("countries",)
def get_queryset(self):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(countries__pk=country_id).distinct()
...@@ -21,8 +21,5 @@ class CountryScholarshipSerializer(ScholarshipSerializer): ...@@ -21,8 +21,5 @@ class CountryScholarshipSerializer(ScholarshipSerializer):
class CountryScholarshipViewSet(ScholarshipViewSet): class CountryScholarshipViewSet(ScholarshipViewSet):
queryset = CountryScholarship.objects.all() # pylint: disable=E1101 queryset = CountryScholarship.objects.all() # pylint: disable=E1101
serializer_class = CountryScholarshipSerializer serializer_class = CountryScholarshipSerializer
end_point_route = r"countryScholarships/(?P<country_id>[a-zA-Z]+)" end_point_route = "countryScholarships"
filterset_fields = ("countries",)
def get_queryset(self):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(countries__pk=country_id).distinct()
...@@ -26,8 +26,5 @@ class CountryTaggedItemSerializer(TaggedItemSerializer): ...@@ -26,8 +26,5 @@ class CountryTaggedItemSerializer(TaggedItemSerializer):
class CountryTaggedItemViewSet(TaggedItemViewSet): class CountryTaggedItemViewSet(TaggedItemViewSet):
queryset = CountryTaggedItem.objects.all() # pylint: disable=E1101 queryset = CountryTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CountryTaggedItemSerializer serializer_class = CountryTaggedItemSerializer
end_point_route = r"countryTaggedItems/(?P<country_id>[a-zA-Z]+)" end_point_route = "countryTaggedItems"
filterset_fields = ("country",)
def get_queryset(self):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(country=country_id).distinct()
...@@ -8,7 +8,6 @@ from backend_app.models.abstract.essentialModule import ( ...@@ -8,7 +8,6 @@ from backend_app.models.abstract.essentialModule import (
EssentialModuleSerializer, EssentialModuleSerializer,
EssentialModuleViewSet, EssentialModuleViewSet,
) )
from backend_app.permissions.app_permissions import ReadOnly
from backend_app.validators.tags import validate_extension from backend_app.validators.tags import validate_extension
...@@ -42,5 +41,4 @@ class UniversitySerializer(EssentialModuleSerializer): ...@@ -42,5 +41,4 @@ class UniversitySerializer(EssentialModuleSerializer):
class UniversityViewSet(EssentialModuleViewSet): class UniversityViewSet(EssentialModuleViewSet):
serializer_class = UniversitySerializer serializer_class = UniversitySerializer
queryset = University.objects.all() # pylint: disable=E1101 queryset = University.objects.all() # pylint: disable=E1101
permission_classes = (ReadOnly,)
end_point_route = "universities" end_point_route = "universities"
...@@ -19,8 +19,5 @@ class UniversityDriViewSet(ModuleViewSet): ...@@ -19,8 +19,5 @@ class UniversityDriViewSet(ModuleViewSet):
queryset = UniversityDri.objects.all() # pylint: disable=E1101 queryset = UniversityDri.objects.all() # pylint: disable=E1101
serializer_class = UniversityDriSerializer serializer_class = UniversityDriSerializer
permission_classes = (IsStaff | IsDri | ReadOnly,) permission_classes = (IsStaff | IsDri | ReadOnly,)
end_point_route = r"universityDri/(?P<univ_id>[0-9]+)" end_point_route = "universityDri"
filterset_fields = ("universities",)
def get_queryset(self):
univ_id = self.kwargs["univ_id"]
return super().get_queryset().filter(universities__pk=univ_id).distinct()
...@@ -23,8 +23,5 @@ class UniversityScholarshipSerializer(ScholarshipSerializer): ...@@ -23,8 +23,5 @@ class UniversityScholarshipSerializer(ScholarshipSerializer):
class UniversityScholarshipViewSet(ScholarshipViewSet): class UniversityScholarshipViewSet(ScholarshipViewSet):
queryset = UniversityScholarship.objects.all() # pylint: disable=E1101 queryset = UniversityScholarship.objects.all() # pylint: disable=E1101
serializer_class = UniversityScholarshipSerializer serializer_class = UniversityScholarshipSerializer
end_point_route = r"universityScholarships/(?P<univ_id>[0-9]+)" end_point_route = "universityScholarships"
filterset_fields = ("universities",)
def get_queryset(self):
univ_id = self.kwargs["univ_id"]
return super().get_queryset().filter(universities__pk=univ_id).distinct()
...@@ -28,8 +28,5 @@ class UniversityTaggedItemViewSet(TaggedItemViewSet): ...@@ -28,8 +28,5 @@ class UniversityTaggedItemViewSet(TaggedItemViewSet):
queryset = UniversityTaggedItem.objects.all() # pylint: disable=E1101 queryset = UniversityTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = UniversityTaggedItemSerializer serializer_class = UniversityTaggedItemSerializer
permission_classes = (IsOwner,) permission_classes = (IsOwner,)
end_point_route = r"universityTaggedItems/(?P<univ_id>[0-9]+)" end_point_route = "universityTaggedItems"
filterset_fields = ("university",)
def get_queryset(self):
univ_id = self.kwargs["univ_id"]
return super().get_queryset().filter(university__pk=univ_id).distinct()
...@@ -56,7 +56,6 @@ class VersionSerializer(BaseModelSerializer): ...@@ -56,7 +56,6 @@ class VersionSerializer(BaseModelSerializer):
djangoSerializers.deserialize(obj.format, data, ignorenonexistent=True) djangoSerializers.deserialize(obj.format, data, ignorenonexistent=True)
)[0] )[0]
# Version is valid, # Version is valid,
print(self.get_serializers_mapping())
obj_serializer = self.get_serializers_mapping()[type(tmp.object).__name__] obj_serializer = self.get_serializers_mapping()[type(tmp.object).__name__]
new_context = dict(self.context) new_context = dict(self.context)
new_context["view"].action = "list" new_context["view"].action = "list"
......
...@@ -210,5 +210,5 @@ class ModerationTestCase(WithUserTestCase): ...@@ -210,5 +210,5 @@ class ModerationTestCase(WithUserTestCase):
c.save() c.save()
data = {"comment": "", "useful_links": [], "universities": [c.pk]} data = {"comment": "", "useful_links": [], "universities": [c.pk]}
api_end_point = "/api/universityDri/{}/".format(c.pk) api_end_point = "/api/universityDri/?universities={}/".format(c.pk)
self._submit_post_test(self.dri_client, data, api_end_point) self._submit_post_test(self.dri_client, data, api_end_point)
...@@ -10,7 +10,7 @@ class WriteAccessTestCase(WithUserTestCase): ...@@ -10,7 +10,7 @@ class WriteAccessTestCase(WithUserTestCase):
@classmethod @classmethod
def setUpMoreTestData(cls): def setUpMoreTestData(cls):
cls.univ = University.objects.create(name="Univ de test", utc_id=100) cls.univ = University.objects.create(name="Univ de test", utc_id=100)
cls.api_dri = "/api/universityDri/{}/".format(cls.univ.pk) cls.api_dri = "/api/universityDri/?universities={}/".format(cls.univ.pk)
cls.post_data = dict( cls.post_data = dict(
universities=[cls.univ.pk], title="qsdlkjqsmlkdj", useful_links="[]" universities=[cls.univ.pk], title="qsdlkjqsmlkdj", useful_links="[]"
) )
......
...@@ -8,7 +8,7 @@ from backend_app.viewsets import ALL_API_VIEWSETS, ALL_API_VIEW_VIEWSETS ...@@ -8,7 +8,7 @@ from backend_app.viewsets import ALL_API_VIEWSETS, ALL_API_VIEW_VIEWSETS
# Building the API routing # Building the API routing
####### #######
urlpatterns = [url(r"^api-docs/", include_docs_urls(title="Outgoing API"))] urlpatterns = [url(r"^api-doc/", include_docs_urls(title="REX-DRI API"))]
# router will hold all api related endpoints # router will hold all api related endpoints
router = routers.DefaultRouter() router = routers.DefaultRouter()
......
...@@ -67,6 +67,7 @@ INSTALLED_APPS = [ ...@@ -67,6 +67,7 @@ INSTALLED_APPS = [
"backend_app", "backend_app",
"base_app", "base_app",
"webpack_loader", "webpack_loader",
"django_filters",
] ]
# #
...@@ -87,6 +88,7 @@ REST_FRAMEWORK = { ...@@ -87,6 +88,7 @@ REST_FRAMEWORK = {
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.TokenAuthentication",
), ),
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
} }
# #
......
...@@ -9,3 +9,5 @@ while [ ! -f `dirname $0`/../frontend/webpack-stats.json ]; do ...@@ -9,3 +9,5 @@ while [ ! -f `dirname $0`/../frontend/webpack-stats.json ]; do
done done
echo "Frontend staticfiles are ready (NB: not necessarly up-to-date)." echo "Frontend staticfiles are ready (NB: not necessarly up-to-date)."
./manage.py collectstatic --noinput && ./manage.py runserver 0.0.0.0:8000
...@@ -3,6 +3,7 @@ Django==2.1.7 ...@@ -3,6 +3,7 @@ Django==2.1.7
psycopg2-binary==2.7.7 psycopg2-binary==2.7.7
django-cas-ng==3.6.0 django-cas-ng==3.6.0
djangorestframework==3.9.1 djangorestframework==3.9.1
django-filter==2.1.0 # easy filtering on API
coreapi==2.3.3 # Automatic API doc generation coreapi==2.3.3 # Automatic API doc generation
django-reversion==3.0.3 django-reversion==3.0.3
django-reversion-compare==0.8.6 django-reversion-compare==0.8.6
......
...@@ -16,6 +16,7 @@ services: ...@@ -16,6 +16,7 @@ services:
image: registry.gitlab.utc.fr/rex-dri/rex-dri/backend:latest image: registry.gitlab.utc.fr/rex-dri/rex-dri/backend:latest
# To use a locally build one, comment above, uncomment bellow. # To use a locally build one, comment above, uncomment bellow.
# build: ./backend # build: ./backend
restart: on-failure
volumes: volumes:
# "Copy" the repo to the workdir. # "Copy" the repo to the workdir.
- .:/usr/src/app/ - .:/usr/src/app/
...@@ -35,10 +36,12 @@ services: ...@@ -35,10 +36,12 @@ services:
- POSTGRES_DB=postgres - POSTGRES_DB=postgres
- POSTGRES_USER=postgres - POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
# Specify host to wait for; ie we want to wait for the db for sure
- WAIT_HOSTS=database:5432
# Run the django developpement server on image startup. # Run the django developpement server on image startup.
command: /bin/sh -c "cd backend && ./waitForFrontend.sh && ./manage.py collectstatic --noinput && ./manage.py runserver 0.0.0.0:8000" command: /bin/sh -c "/wait && cd backend && ./entry.sh"
depends_on: depends_on:
# Required that the `database` and `frontend` service is up and running. # Required that the `database` and `frontend` service are up and running.
- database - database
- frontend - frontend
......
...@@ -5,7 +5,7 @@ API ...@@ -5,7 +5,7 @@ API
The backend `Django` app, with `Django Rest Framework` acts as an API. It is available at the URI `/api`. The backend `Django` app, with `Django Rest Framework` acts as an API. It is available at the URI `/api`.
An automated documentation is generated and available at the URI `/api-docs`. An automated documentation is generated and available at the URI `/api-doc`.
## Authentication ## Authentication
......
...@@ -141,6 +141,7 @@ Just like in the standard use of the Django Rest Framework, you should set the f ...@@ -141,6 +141,7 @@ Just like in the standard use of the Django Rest Framework, you should set the f
* `queryset` (or `get_queryset`): the queryset associated with the viewset, * `queryset` (or `get_queryset`): the queryset associated with the viewset,
* `serializer_class`: what is the serializer class to use to serialize the queryset. * `serializer_class`: what is the serializer class to use to serialize the queryset.
* `viewset_permission` (defaults to `(IsAuthenticated & (IsStaff | NoDelete))`): what are the permissions associated with each viewset. If something is specified then it will be composed (`&`) with the default one. You can compose permissions with `&` (logical *and*) or `|` (logical *or*). Most permissions are defined in `backend/backend_app/permissions/app_permissions.py`, you can add yours too. * `viewset_permission` (defaults to `(IsAuthenticated & (IsStaff | NoDelete))`): what are the permissions associated with each viewset. If something is specified then it will be composed (`&`) with the default one. You can compose permissions with `&` (logical *and*) or `|` (logical *or*). Most permissions are defined in `backend/backend_app/permissions/app_permissions.py`, you can add yours too.
* `filterset_fields` (optional): more information [here](Application/Backend/models_serializers_viewsets?id=filtering)
?> :information_desk_person: If you set `viewset_permission`, it must be a tuple; (in our case it will often have only one attribute). ?> :information_desk_person: If you set `viewset_permission`, it must be a tuple; (in our case it will often have only one attribute).
...@@ -165,19 +166,54 @@ class CountryViewSet(BaseModelViewSet): ...@@ -165,19 +166,54 @@ class CountryViewSet(BaseModelViewSet):
end_point_route = "countries" end_point_route = "countries"
``` ```
Sometimes you will also need to filter the data against some attribute found in the client's request: #### Filtering
Sometimes you will also need to filter the data against some attribute found in the client's request.
You should favor the standard approach to do so. If the endpoint is `/api/endpoint` and you want to make sure the model attribute `attr` equals `x` for the object returned then the client should fetch the following route: `/api/endpoint?attr=x` (if multiple attributes need to be filtered then it would look like `/api/endpoint?attr1=x&attr2=y&attr3=z`).
To make it work like this, you must add `filterset_fields` to your viewsets. It should be a tuple of the names of the fields you allow filtering on.
```python ```python
class CountryDriViewSet(ModuleViewSet): class CountryDriViewSet(ModuleViewSet):
queryset = CountryDri.objects.all() # pylint: disable=E1101 queryset = CountryDri.objects.all() # pylint: disable=E1101
serializer_class = CountryDriSerializer serializer_class = CountryDriSerializer
permission_classes = (IsStaff | IsDri | NoPost,)
end_point_route = "countryDri"
filterset_fields = ("countries",)
```
This filtering is achieved through [`django-filter`](https://django-filter.readthedocs.io/en/master/) package (and the [standard api provided by `django-rest-framework`](https://www.django-rest-framework.org/api-guide/filtering/)), you can further customize the filtering if you'd like to.
------
There exist an other way of filtering directly on the client request, **but you should use it only when there are absolutely no other alternatives or when parameters are absolutely required**. You can see this other way below:
* Endpoint's attributes are set to be captured in `end_point_route`,
* We make use of them in `get_queryset`.
```python
class VersionViewSet(BaseModelViewSet):
# ...
end_point_route = (
r"versions/(?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+)"
)
def get_queryset(self): def get_queryset(self):
country_id = self.kwargs["country_id"] content_type_id = self.kwargs["content_type_id"]
return super().get_queryset().filter(countries__pk=country_id).distinct() object_pk = self.kwargs["object_pk"]
ct = ContentType.objects.get_for_id(content_type_id)
model = ct.model_class()
obj = model.objects.get(pk=object_pk)
return Version.objects.get_for_object(obj)
``` ```
Or also prefetch some related attributes for optimization purposes:
#### Performance
!> **Viewsets can be huge bottlenecks for performances.**
If the objects the viewset will be serializing have a lot of "foreign" data (from a ForeignKey, ManyToMany field, etc.), you **shall** extend the queryset to make sure to prefetch all the data needed for the serializer:
```python ```python
# Extract of the class definition # Extract of the class definition
...@@ -192,10 +228,7 @@ class EssentialModuleViewSet(BaseModelViewSet): ...@@ -192,10 +228,7 @@ class EssentialModuleViewSet(BaseModelViewSet):
return self.queryset.prefetch_related("moderated_by", "updated_by") return self.queryset.prefetch_related("moderated_by", "updated_by")
``` ```
TODO below will be outdated soon ?> :information_desk_person: This will basically generate an SQL `JOIN` once per queryset. If you don't do this, the serialization of each attribute will result to one `SQL` query, leading to hundreds of them on a simple client request.
?> If you are familiar with `REST` api, you will surely be familiar with filtering elements directly in the request with a syntaxe such as `url/?username=bob&isOld=true`. This syntax is not yet supported, since we didn't need it up until now.
## Keeping things consistent ## Keeping things consistent
......
...@@ -225,13 +225,19 @@ const mapDispatchToProps = (dispatch) => { ...@@ -225,13 +225,19 @@ const mapDispatchToProps = (dispatch) => {
The `getActions` function will give you access to all of the following functions: The `getActions` function will give you access to all of the following functions:
- `readAll()`: reads all the data from the endpoint (data will be given as an array of object) - `readAll(params={})`: reads all the data from the endpoint (data will be given as an array of object)
- `readSpecific(id)`: reads the data for an instance (the `id` is the instance's `id`/primary key) (data will be returned as an object) - `readSpecific(id, params={})`: reads the data for an instance (the `id` is the instance's `id`/primary key) (data will be returned as an object)
- `create(data)`: creates an object on the endpoint with attributes values given by `data`. - `create(data)`: creates an object on the endpoint with attributes values given by `data`.
- `update(data)` (`data` must have an `id` field): updates the instance identified by `id`. - `update(data)` (`data` must have an `id` field): updates the instance identified by `id`.
- `setInvalidatedAll()`: invalidates "all" the data for the endpoint, - `setInvalidatedAll()`: invalidates "all" the data for the endpoint,
- `setInvalidatedSpecific()`: invalidates "specific" data from the endpoint. - `setInvalidatedSpecific()`: invalidates "specific" data from the endpoint.
?> `params` is an object that accepts two keys: `queryParams` and `endPointAttr`. In most cases it should be created through the `getQueryParams` and `getEndPointAttr` (and `mapDispatchToProps`; see just before [this section](Application/Frontend/redux?id=words-on-updating-data-on-the-api)) functions of your components.
> * `queryParams` should be an object that maps the fields to the values you want to filter on (`queryParams = {university: 1, country: 2}` will render as `/api/endpoint?university=1&country=2`).
> * `endPointAttr` should be an array of the endpoint attributes to add to the endpoint (`endPointAttr = [10, 11]` will render as `/api/endpoint/10/11/`)
!> Not all actions might be performed on any given endpoint: your request my get rejected by the backend depending on the viewset's `permission_classes`, the object, the user who sent the request, etc. !> Not all actions might be performed on any given endpoint: your request my get rejected by the backend depending on the viewset's `permission_classes`, the object, the user who sent the request, etc.
?> :information_desk_person: invalidating data will usually trigger a refresh of that data with a new API call. ?> :information_desk_person: invalidating data will usually trigger a refresh of that data with a new API call.
...@@ -282,7 +288,11 @@ const mapDispatchToProps = (dispatch) => { ...@@ -282,7 +288,11 @@ const mapDispatchToProps = (dispatch) => {
!> **The function that will be used to dispatch the action associated with fetching the data for `propName` must be identified by `api.propName` in the `mapDispatchToProps` function.** !> **The function that will be used to dispatch the action associated with fetching the data for `propName` must be identified by `api.propName` in the `mapDispatchToProps` function.**
That's all :confetti_ball:. <div id="customGettersApi"></div>
?> :information_desk_person: All the subfunctions you define in `mapDispatchToProps` under the `api` key will be given one object parameter you may want to use. It is composed as follow: `{props, params}`. `props` will be an object corresponding to the `props` of the *connected* component. `params` is built with the `getQueryParams` and `getEndPointAttr` you may wish to override in your connected components. **`params` shall be *passed on* for filtering to work correctly**.
That's all :confetti_ball:.
### Words on updating data on the API ### Words on updating data on the API
......
...@@ -146,7 +146,7 @@ const mapStateToProps = (state) => { ...@@ -146,7 +146,7 @@ const mapStateToProps = (state) => {
return { return {
countries: state.api.countriesAll, countries: state.api.countriesAll,
currencies: state.api.currenciesAll, currencies: state.api.currenciesAll,
serverModerationStatus: state.api.serverModerationStatusSpecific, serverModerationStatus: state.api.serverModerationStatusAll,
}; };
}; };
...@@ -155,7 +155,7 @@ const mapDispatchToProps = (dispatch) => { ...@@ -155,7 +155,7 @@ const mapDispatchToProps = (dispatch) => {
api: { api: {
countries: () => dispatch(getActions("countries").readAll()), countries: () => dispatch(getActions("countries").readAll()),
currencies: () => dispatch(getActions("currencies").readAll()), currencies: () => dispatch(getActions("currencies").readAll()),
serverModerationStatus: () => dispatch(getActions("serverModerationStatus").readSpecific("")), // not needed for server moderation status serverModerationStatus: () => dispatch(getActions("serverModerationStatus").readAll()), // not needed for server moderation status
}, },
}; };
}; };
......
...@@ -44,18 +44,30 @@ const mapStateToProps = (state) => { ...@@ -44,18 +44,30 @@ const mapStateToProps = (state) => {
*/ */
class CustomComponentForAPI extends Component { class CustomComponentForAPI extends Component {
customErrorHandlers = {} customErrorHandlers = {}
// __apiAttr should be an object
// mapping the prop that needs to fetched with extra api attributes
// mapping should be : props_key => other_props_that contains the attribute to use
__apiAttr = null;
// __apiParam should be an object /**
// mapping the prop that needs to be fetched to the parameter to be passed to the read function * A function with a propName parameter that should return the query parameters to add when reading that propName
__apiParam = null; */
// eslint-disable-next-line no-unused-vars
getQueryParams = (propName) => undefined;
/**
* A function with a propName parameter that should return the endPoint attrs to add when reading that propName
*/
// eslint-disable-next-line no-unused-vars
getEndPointAttr = (propName) => undefined;
constructor(props) { constructor(props) {
super(props); super(props);
if (typeof this.getEndPointAttr !== "function") {
throw new Error("getEndPointAttr misconfigured: it must be a function");
}
if (typeof this.getQueryParams !== "function") {
throw new Error("getQueryParams misconfigured: it must be a function");
}
// a little bit of optimization // a little bit of optimization
// Stores the list of props that use the API // Stores the list of props that use the API
this.apiProps = Array(); this.apiProps = Array();
...@@ -185,13 +197,21 @@ class CustomComponentForAPI extends Component { ...@@ -185,13 +197,21 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI * @memberof CustomComponentForAPI
*/ */
performReadFromApi(propName) { performReadFromApi(propName) {
if (this.__apiAttr && this.__apiAttr[propName]) {
this.props.api[propName](this.props[this.__apiAttr[propName]]); let endPointAttr = "",
} else if (this.__apiParam && this.__apiParam[propName]) { queryParams = {};
this.props.api[propName](this.__apiParam[propName]);
} else { const tmpQueryParams = this.getQueryParams(propName);
this.props.api[propName](); if (tmpQueryParams) {
queryParams = tmpQueryParams;
}
const tmpEndPointAttr = this.getEndPointAttr(propName);
if (tmpEndPointAttr) {
endPointAttr = tmpEndPointAttr;
} }
this.props.api[propName]({ params: { queryParams, endPointAttr }, props: this.props });
} }
/** /**
......
...@@ -154,7 +154,7 @@ class Editor extends Component { ...@@ -154,7 +154,7 @@ class Editor extends Component {
performSave(data) { performSave(data) {
this.notifyIsSaving(); this.notifyIsSaving();
this.props.saveData( this.props.saveData(
Object.assign({ __apiAttr: this.props.__apiAttr }, data), data,
(newData) => this.handleSaveRequestWasSuccessful(newData) (newData) => this.handleSaveRequestWasSuccessful(newData)
); );
} }
...@@ -367,7 +367,6 @@ Editor.propTypes = { ...@@ -367,7 +367,6 @@ Editor.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired, // should the editor be opened open: PropTypes.bool.isRequired, // should the editor be opened
subscribeToModuleWrapper: PropTypes.func.isRequired, subscribeToModuleWrapper: PropTypes.func.isRequired,
__apiAttr: PropTypes.oneOf([PropTypes.number, PropTypes.string, ""]),
rawModelData: PropTypes.object.isRequired, rawModelData: PropTypes.object.isRequired,
closeEditorPanel: PropTypes.func.isRequired, closeEditorPanel: PropTypes.func.isRequired,
// props added in subclasses but are absolutely required to handle redux // props added in subclasses but are absolutely required to handle redux
...@@ -384,7 +383,6 @@ Editor.propTypes = { ...@@ -384,7 +383,6 @@ Editor.propTypes = {
Editor.defaultProps = { Editor.defaultProps = {
open: false, open: false,
__apiAttr: "",
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
closeEditorPanel: () => console.error("Dev forgot something...") closeEditorPanel: () => console.error("Dev forgot something...")
}; };
......
...@@ -28,6 +28,11 @@ function renderCore(rawModelData, classes, outsideData) { ...@@ -28,6 +28,11 @@ function renderCore(rawModelData, classes, outsideData) {
} }
class CountryDri extends Module { class CountryDri extends Module {
/**
* @override
*/
getQueryParams = (propName) => propName === "countryDri" ? ({ countries: this.props.countryId }) : undefined;
customRender() { customRender() {
const countryDriItems = this.getOnlyReadData("countryDri"), const countryDriItems = this.getOnlyReadData("countryDri"),
{ countries, classes } = this.props, { countries, classes } = this.props,
...@@ -42,7 +47,6 @@ class CountryDri extends Module { ...@@ -42,7 +47,6 @@ class CountryDri extends Module {
defaultModelData={{ countries: [this.props.countryId], importance_level: "-" }} defaultModelData={{ countries: [this.props.countryId], importance_level: "-" }}
propsForEditor={{ propsForEditor={{
outsideData: outsideData, outsideData: outsideData,
__apiAttr: this.props.countryId,
}} }}
> >
{ {
...@@ -56,7 +60,6 @@ class CountryDri extends Module { ...@@ -56,7 +60,6 @@ class CountryDri extends Module {
coreClasses={classes} coreClasses={classes}
outsideData={outsideData} outsideData={outsideData}
moduleInGroupInfos={{ isInGroup: true, invalidateGroup: () => this.props.invalidateData() }} moduleInGroupInfos={{ isInGroup: true, invalidateGroup: () => this.props.invalidateData() }}
__apiAttr={this.props.countryId}
/> />
)) ))
} }
...@@ -64,8 +67,6 @@ class CountryDri extends Module { ...@@ -64,8 +67,6 @@ class CountryDri extends Module {
); );
} }
__apiAttr = { countryDri: "countryId" };
} }
CountryDri.propTypes = { CountryDri.propTypes = {
...@@ -77,16 +78,16 @@ CountryDri.propTypes = { ...@@ -77,16 +78,16 @@ CountryDri.propTypes = {
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
return { return {
countryDri: state.api.countryDriSpecific, countryDri: state.api.countryDriAll,
}; };
}; };
const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return { return {
api: { api: {
countryDri: (countryId) => dispatch(getActions("countryDri").readSpecific(countryId)), countryDri: ({ params }) => dispatch(getActions("countryDri").readAll(params)),
}, },
invalidateData: () => dispatch(getActions("countryDri").setInvalidatedSpecific(true)) invalidateData: () => dispatch(getActions("countryDri").setInvalidatedAll(true))
}; };
}; };
......
...@@ -43,6 +43,12 @@ function renderCore(rawModelData, classes, outsideData) { ...@@ -43,6 +43,12 @@ function renderCore(rawModelData, classes, outsideData) {