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
......@@ -21,3 +21,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN pip install --upgrade pip
COPY requirements.txt /usr/src/app/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):
This allows to not do this query for all elements and improves
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")
user_can_edit = True
......
......@@ -26,8 +26,5 @@ class CampusTaggedItemSerializer(TaggedItemSerializer):
class CampusTaggedItemViewSet(TaggedItemViewSet):
queryset = CampusTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CampusTaggedItemSerializer
end_point_route = r"campusTaggedItems/(?P<campus_id>[0-9]+)"
def get_queryset(self):
campus_id = self.kwargs["campus_id"]
return super().get_queryset().filter(campus=campus_id).distinct()
end_point_route = "campusTaggedItems"
filterset_fields = ("campus",)
......@@ -26,8 +26,5 @@ class CityTaggedItemSerializer(TaggedItemSerializer):
class CityTaggedItemViewSet(TaggedItemViewSet):
queryset = CityTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CityTaggedItemSerializer
end_point_route = r"cityTaggedItems/(?P<city_id>[0-9]+)"
def get_queryset(self):
city_id = self.kwargs["city_id"]
return super().get_queryset().filter(city=city_id).distinct()
end_point_route = "cityTaggedItems"
filterset_fields = ("city",)
......@@ -19,8 +19,5 @@ class CountryDriViewSet(ModuleViewSet):
queryset = CountryDri.objects.all() # pylint: disable=E1101
serializer_class = CountryDriSerializer
permission_classes = (IsStaff | IsDri | NoPost,)
end_point_route = r"countryDri/(?P<country_id>[a-zA-Z]+)"
def get_queryset(self):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(countries__pk=country_id).distinct()
end_point_route = "countryDri"
filterset_fields = ("countries",)
......@@ -21,8 +21,5 @@ class CountryScholarshipSerializer(ScholarshipSerializer):
class CountryScholarshipViewSet(ScholarshipViewSet):
queryset = CountryScholarship.objects.all() # pylint: disable=E1101
serializer_class = CountryScholarshipSerializer
end_point_route = r"countryScholarships/(?P<country_id>[a-zA-Z]+)"
def get_queryset(self):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(countries__pk=country_id).distinct()
end_point_route = "countryScholarships"
filterset_fields = ("countries",)
......@@ -26,8 +26,5 @@ class CountryTaggedItemSerializer(TaggedItemSerializer):
class CountryTaggedItemViewSet(TaggedItemViewSet):
queryset = CountryTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CountryTaggedItemSerializer
end_point_route = r"countryTaggedItems/(?P<country_id>[a-zA-Z]+)"
def get_queryset(self):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(country=country_id).distinct()
end_point_route = "countryTaggedItems"
filterset_fields = ("country",)
......@@ -8,7 +8,6 @@ from backend_app.models.abstract.essentialModule import (
EssentialModuleSerializer,
EssentialModuleViewSet,
)
from backend_app.permissions.app_permissions import ReadOnly
from backend_app.validators.tags import validate_extension
......@@ -42,5 +41,4 @@ class UniversitySerializer(EssentialModuleSerializer):
class UniversityViewSet(EssentialModuleViewSet):
serializer_class = UniversitySerializer
queryset = University.objects.all() # pylint: disable=E1101
permission_classes = (ReadOnly,)
end_point_route = "universities"
......@@ -19,8 +19,5 @@ class UniversityDriViewSet(ModuleViewSet):
queryset = UniversityDri.objects.all() # pylint: disable=E1101
serializer_class = UniversityDriSerializer
permission_classes = (IsStaff | IsDri | ReadOnly,)
end_point_route = r"universityDri/(?P<univ_id>[0-9]+)"
def get_queryset(self):
univ_id = self.kwargs["univ_id"]
return super().get_queryset().filter(universities__pk=univ_id).distinct()
end_point_route = "universityDri"
filterset_fields = ("universities",)
......@@ -23,8 +23,5 @@ class UniversityScholarshipSerializer(ScholarshipSerializer):
class UniversityScholarshipViewSet(ScholarshipViewSet):
queryset = UniversityScholarship.objects.all() # pylint: disable=E1101
serializer_class = UniversityScholarshipSerializer
end_point_route = r"universityScholarships/(?P<univ_id>[0-9]+)"
def get_queryset(self):
univ_id = self.kwargs["univ_id"]
return super().get_queryset().filter(universities__pk=univ_id).distinct()
end_point_route = "universityScholarships"
filterset_fields = ("universities",)
......@@ -28,8 +28,5 @@ class UniversityTaggedItemViewSet(TaggedItemViewSet):
queryset = UniversityTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = UniversityTaggedItemSerializer
permission_classes = (IsOwner,)
end_point_route = r"universityTaggedItems/(?P<univ_id>[0-9]+)"
def get_queryset(self):
univ_id = self.kwargs["univ_id"]
return super().get_queryset().filter(university__pk=univ_id).distinct()
end_point_route = "universityTaggedItems"
filterset_fields = ("university",)
......@@ -56,7 +56,6 @@ class VersionSerializer(BaseModelSerializer):
djangoSerializers.deserialize(obj.format, data, ignorenonexistent=True)
)[0]
# Version is valid,
print(self.get_serializers_mapping())
obj_serializer = self.get_serializers_mapping()[type(tmp.object).__name__]
new_context = dict(self.context)
new_context["view"].action = "list"
......
......@@ -210,5 +210,5 @@ class ModerationTestCase(WithUserTestCase):
c.save()
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)
......@@ -10,7 +10,7 @@ class WriteAccessTestCase(WithUserTestCase):
@classmethod
def setUpMoreTestData(cls):
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(
universities=[cls.univ.pk], title="qsdlkjqsmlkdj", useful_links="[]"
)
......
......@@ -8,7 +8,7 @@ from backend_app.viewsets import ALL_API_VIEWSETS, ALL_API_VIEW_VIEWSETS
# 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 = routers.DefaultRouter()
......
......@@ -67,6 +67,7 @@ INSTALLED_APPS = [
"backend_app",
"base_app",
"webpack_loader",
"django_filters",
]
#
......@@ -87,6 +88,7 @@ REST_FRAMEWORK = {
"rest_framework.authentication.SessionAuthentication",
"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
done
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
psycopg2-binary==2.7.7
django-cas-ng==3.6.0
djangorestframework==3.9.1
django-filter==2.1.0 # easy filtering on API
coreapi==2.3.3 # Automatic API doc generation
django-reversion==3.0.3
django-reversion-compare==0.8.6
......
......@@ -16,6 +16,7 @@ services:
image: registry.gitlab.utc.fr/rex-dri/rex-dri/backend:latest
# To use a locally build one, comment above, uncomment bellow.
# build: ./backend
restart: on-failure
volumes:
# "Copy" the repo to the workdir.
- .:/usr/src/app/
......@@ -35,10 +36,12 @@ services:
- POSTGRES_DB=postgres
- POSTGRES_USER=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.
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:
# Required that the `database` and `frontend` service is up and running.
# Required that the `database` and `frontend` service are up and running.
- database
- frontend
......
......@@ -5,7 +5,7 @@ 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
......
......@@ -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,
* `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.
* `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).
......@@ -165,19 +166,54 @@ class CountryViewSet(BaseModelViewSet):
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
class CountryDriViewSet(ModuleViewSet):
queryset = CountryDri.objects.all() # pylint: disable=E1101
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):
country_id = self.kwargs["country_id"]
return super().get_queryset().filter(countries__pk=country_id).distinct()
content_type_id = self.kwargs["content_type_id"]
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
# Extract of the class definition
......@@ -192,10 +228,7 @@ class EssentialModuleViewSet(BaseModelViewSet):
return self.queryset.prefetch_related("moderated_by", "updated_by")
```
TODO below will be outdated soon
?> 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.
?> :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.
## Keeping things consistent
......
......@@ -225,13 +225,19 @@ const mapDispatchToProps = (dispatch) => {
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)
- `readSpecific(id)`: reads the data for an instance (the `id` is the instance's `id`/primary key) (data will be returned as an object)
- `readAll(params={})`: reads all the data from the endpoint (data will be given as an array of 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`.
- `update(data)` (`data` must have an `id` field): updates the instance identified by `id`.
- `setInvalidatedAll()`: invalidates "all" the data for 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.
?> :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) => {
!> **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
......
......@@ -146,7 +146,7 @@ const mapStateToProps = (state) => {
return {
countries: state.api.countriesAll,
currencies: state.api.currenciesAll,
serverModerationStatus: state.api.serverModerationStatusSpecific,
serverModerationStatus: state.api.serverModerationStatusAll,
};
};
......@@ -155,7 +155,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
countries: () => dispatch(getActions("countries").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) => {
*/
class CustomComponentForAPI extends Component {
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
__apiParam = null;
/**
* A function with a propName parameter that should return the query parameters to add when reading that propName
*/
// 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) {
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
// Stores the list of props that use the API
this.apiProps = Array();
......@@ -185,13 +197,21 @@ class CustomComponentForAPI extends Component {
* @memberof CustomComponentForAPI
*/
performReadFromApi(propName) {
if (this.__apiAttr && this.__apiAttr[propName]) {
this.props.api[propName](this.props[this.__apiAttr[propName]]);
} else if (this.__apiParam && this.__apiParam[propName]) {
this.props.api[propName](this.__apiParam[propName]);
} else {
this.props.api[propName]();
let endPointAttr = "",
queryParams = {};
const tmpQueryParams = this.getQueryParams(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 {
performSave(data) {
this.notifyIsSaving();
this.props.saveData(
Object.assign({ __apiAttr: this.props.__apiAttr }, data),
data,
(newData) => this.handleSaveRequestWasSuccessful(newData)
);
}
......@@ -367,7 +367,6 @@ Editor.propTypes = {
classes: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired, // should the editor be opened
subscribeToModuleWrapper: PropTypes.func.isRequired,
__apiAttr: PropTypes.oneOf([PropTypes.number, PropTypes.string, ""]),
rawModelData: PropTypes.object.isRequired,
closeEditorPanel: PropTypes.func.isRequired,
// props added in subclasses but are absolutely required to handle redux
......@@ -384,7 +383,6 @@ Editor.propTypes = {
Editor.defaultProps = {
open: false,
__apiAttr: "",
// eslint-disable-next-line no-console
closeEditorPanel: () => console.error("Dev forgot something...")
};
......
......@@ -28,6 +28,11 @@ function renderCore(rawModelData, classes, outsideData) {
}
class CountryDri extends Module {
/**
* @override
*/
getQueryParams = (propName) => propName === "countryDri" ? ({ countries: this.props.countryId }) : undefined;
customRender() {
const countryDriItems = this.getOnlyReadData("countryDri"),
{ countries, classes } = this.props,
......@@ -42,7 +47,6 @@ class CountryDri extends Module {
defaultModelData={{ countries: [this.props.countryId], importance_level: "-" }}
propsForEditor={{
outsideData: outsideData,
__apiAttr: this.props.countryId,
}}
>
{
......@@ -56,7 +60,6 @@ class CountryDri extends Module {
coreClasses={classes}
outsideData={outsideData}
moduleInGroupInfos={{ isInGroup: true, invalidateGroup: () => this.props.invalidateData() }}
__apiAttr={this.props.countryId}
/>
))
}
......@@ -64,8 +67,6 @@ class CountryDri extends Module {
);
}
__apiAttr = { countryDri: "countryId" };
}
CountryDri.propTypes = {
......@@ -77,16 +78,16 @@ CountryDri.propTypes = {
const mapStateToProps = (state) => {
return {
countryDri: state.api.countryDriSpecific,
countryDri: state.api.countryDriAll,
};
};
const mapDispatchToProps = (dispatch) => {
return {
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) {
class CountryScholarships extends Module {
/**
* @override
*/
getQueryParams = (propName) => propName === "countryScholarships" ? ({ countries: this.props.countryId }) : undefined;
customRender() {
const countryScholarshipsItems = this.getOnlyReadData("countryScholarships"),
{ countries, currencies, classes } = this.props,
......@@ -60,7 +66,6 @@ class CountryScholarships extends Module {
defaultModelData={{ countries: [this.props.countryId], importance_level: "-", currency: "EUR", obj_moderation_level: 0 }}
propsForEditor={{
outsideData: outsideData,
__apiAttr: this.props.countryId,
}}
>
{
......@@ -74,7 +79,6 @@ class CountryScholarships extends Module {
coreClasses={classes}
outsideData={outsideData}
moduleInGroupInfos={{ isInGroup: true, invalidateGroup: () => this.props.invalidateData() }}
__apiAttr={this.props.countryId}
/>
))
}
......@@ -82,8 +86,6 @@ class CountryScholarships extends Module {
);
}
__apiAttr = { countryScholarships: "countryId" };
}
CountryScholarships.propTypes = {
......@@ -97,16 +99,16 @@ CountryScholarships.propTypes = {
const mapStateToProps = (state) => {
return {
countryScholarships: state.api.countryScholarshipsSpecific,
countryScholarships: state.api.countryScholarshipsAll,
};
};
const mapDispatchToProps = (dispatch) => {
return {
api: {
countryScholarships: (countryId) => dispatch(getActions("countryScholarships").readSpecific(countryId)),
countryScholarships: ({ params }) => dispatch(getActions("countryScholarships").readAll(params)),
},
invalidateData: () => dispatch(getActions("countryScholarships").setInvalidatedSpecific(true))
invalidateData: () => dispatch(getActions("countryScholarships").setInvalidatedAll(true))
};
};
......
......@@ -32,10 +32,15 @@ function renderCore(rawModelData, classes, outsideData) {
class UniversityDri extends Module {
/**
* @override
*/
getQueryParams = (propName) => propName === "universityDri" ? ({ universities: this.props.univId }) : undefined;
customRender() {
const { universities, classes } = this.props,
outsideData = { universities },
univDriItems = this.getLatestReadData("universityDri", false);
univDriItems = this.getLatestReadData("universityDri");
return (
<ModuleGroupWrapper
......@@ -46,7 +51,6 @@ class UniversityDri extends Module {
defaultModelData={{ universities: [this.props.univId], importance_level: "-" }}
propsForEditor={{
outsideData: outsideData,
__apiAttr: this.props.univId,
}}
>
{
......@@ -60,7 +64,6 @@ class UniversityDri extends Module {
coreClasses={classes}
outsideData={outsideData}
moduleInGroupInfos={{ isInGroup: true, invalidateGroup: () => this.props.invalidateData() }}
__apiAttr={this.props.univId}
/>
))
}
......@@ -68,8 +71,6 @@ class UniversityDri extends Module {
);
}
__apiAttr = { universityDri: "univId" };
}
UniversityDri.propTypes = {
......@@ -81,16 +82,16 @@ UniversityDri.propTypes = {
const mapStateToProps = (state) => {
return {
universityDri: state.api.universityDriSpecific,
universityDri: state.api.universityDriAll,
};
};
const mapDispatchToProps = (dispatch) => {
return {
api: {
universityDri: (univId) => dispatch(getActions("universityDri").readSpecific(univId)),
universityDri: ({ params }) => dispatch(getActions("universityDri").readAll(params)),
},
invalidateData: () => dispatch(getActions("universityDri").setInvalidatedSpecific(true))
invalidateData: () => dispatch(getActions("universityDri").setInvalidatedAll(true))
};
};
......
......@@ -76,8 +76,6 @@ class UniversityGeneral extends Module {
/>
);
}
__apiAttr = { university: "univId" };
}
UniversityGeneral.propTypes = {
......@@ -97,7 +95,7 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => {
return {
api: {
university: (univId) => dispatch(getActions("universities").readSpecific(univId)),
university: ({ props }) => dispatch(getActions("universities").readSpecific(props.univId)),
},
invalidateData: () => dispatch(getActions("universities").setInvalidatedSpecific(true))
};
......
......@@ -38,6 +38,12 @@ function renderCore(rawModelData, classes, outsideData) {
}
class UniversityScholarships extends Module {
/**
* @override
*/
getQueryParams = (propName) => propName === "universityScholarships" ? ({ universities: this.props.univId, }) : undefined;
customRender() {
const univScholarshipsItems = this.getOnlyReadData("universityScholarships"),
{ universities, currencies, classes } = this.props,
......@@ -55,7 +61,6 @@ class UniversityScholarships extends Module {
defaultModelData={{ universities: [this.props.univId], importance_level: "-", currency: "EUR", obj_moderation_level: 0 }}
propsForEditor={{
outsideData: outsideData,
__apiAttr: this.props.univId,
}}
>
{
......@@ -69,7 +74,6 @@ class UniversityScholarships extends Module {
coreClasses={classes}
outsideData={outsideData}
moduleInGroupInfos={{ isInGroup: true, invalidateGroup: () => this.props.invalidateData() }}
__apiAttr={this.props.univId}
/>
))
}
......@@ -77,8 +81,6 @@ class UniversityScholarships extends Module {
);
}
__apiAttr = { universityScholarships: "univId" };
}
UniversityScholarships.propTypes = {
......@@ -91,16 +93,16 @@ UniversityScholarships.propTypes = {
const mapStateToProps = (state) => {
return {
universityScholarships: state.api.universityScholarshipsSpecific,
universityScholarships: state.api.universityScholarshipsAll,
};
};
const mapDispatchToProps = (dispatch) => {
return {