Commit 1237854b authored by Florent Chehab's avatar Florent Chehab

Merge branch 'cleaning' into 'master'

Cleaning

Closes #48 and #47

See merge request rex-dri/rex-dri!48
parents 909dae0d 827fa5b8
Pipeline #35341 passed with stages
in 3 minutes and 59 seconds
{
"python.pythonPath": "env/bin/python",
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/__pycache__": true,
"**/*.pyc": true,
"htmlcov": false,
"env": true,
".coverage": true,
".pytest_cache": true,
"backend/htmlcov": false,
"backend/.coverage": true,
"backend/.pytest_cache": true,
},
"cSpell.language": "en,fr-FR,fr",
"python.linting.flake8Enabled": true,
......
from rest_framework import viewsets
class DictModeViewSet(viewsets.ModelViewSet):
"""
ViewSet that renders data as dict with keys corresponding to the model
primary key. Instead of list.
"""
BYPASS_DICT_MODE = False
LIST_SHOULD_BE_DETAIL = False
def list(self, request, *args, **kwargs):
response = super(viewsets.ModelViewSet, self).list( # pylint: disable=E1003
request, *args, **kwargs
) # call the original 'list'
if self.LIST_SHOULD_BE_DETAIL:
if len(response.data) == 0:
response.data = dict()
elif len(response.data) == 1:
response.data = response.data[0]
else:
raise Exception(
"There should be no more than one element here check your queryset !"
)
elif not self.BYPASS_DICT_MODE:
pk_attr_name = self.serializer_class.Meta.model._meta.pk.name
response.data = {d[pk_attr_name]: d for d in response.data}
return response
from .DictModeViewSet import DictModeViewSet
from .mySerializerWithJSON import MySerializerWithJSON
__all__ = ["DictModeViewSet", "MySerializerWithJSON"]
__all__ = ["MySerializerWithJSON"]
from .myModelSerializer import MyModelSerializer
from backend_app.permissions import DEFAULT_VIEWSET_PERMISSIONS
from backend_app.custom import DictModeViewSet
from rest_framework import viewsets
class MyModelViewSet(DictModeViewSet):
class MyModelViewSet(viewsets.ModelViewSet):
serializer_class = MyModelSerializer
permission_classes = DEFAULT_VIEWSET_PERMISSIONS
LIST_SHOULD_BE_DETAIL = False
def list(self, request, *args, **kwargs):
response = super(viewsets.ModelViewSet, self).list( # pylint: disable=E1003
request, *args, **kwargs
) # call the original 'list'
if self.LIST_SHOULD_BE_DETAIL:
if len(response.data) == 0:
response.data = dict()
elif len(response.data) == 1:
response.data = response.data[0]
else:
raise Exception(
"There should be no more than one element in the queryset, check it !"
)
return response
def get_queryset(self):
"""
Extended default rest framework behavior
......
......@@ -33,8 +33,6 @@ class CampusTaggedItemViewSet(TaggedItemViewSet):
queryset = CampusTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CampusTaggedItemSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
campus_id = self.kwargs["campus_id"]
return self.my_model_queryset.filter(campus=campus_id).distinct()
......@@ -34,8 +34,6 @@ class CityTaggedItemViewSet(TaggedItemViewSet):
queryset = CityTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CityTaggedItemSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
city_id = self.kwargs["city_id"]
return self.my_model_queryset.filter(city=city_id).distinct()
......@@ -30,8 +30,6 @@ class CountryDriViewSet(BasicModuleViewSet):
queryset = CountryDri.objects.all() # pylint: disable=E1101
serializer_class = CountryDriSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
country_id = self.kwargs["country_id"]
return self.my_model_queryset.filter(countries__pk=country_id).distinct()
......@@ -28,8 +28,6 @@ class CountryScholarshipViewSet(ScholarshipViewSet):
queryset = CountryScholarship.objects.all() # pylint: disable=E1101
serializer_class = CountryScholarshipSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
country_id = self.kwargs["country_id"]
return self.my_model_queryset.filter(countries__pk=country_id).distinct()
......@@ -33,8 +33,6 @@ class CountryTaggedItemViewSet(TaggedItemViewSet):
queryset = CountryTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CountryTaggedItemSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
country_id = self.kwargs["country_id"]
return self.my_model_queryset.filter(country=country_id).distinct()
......@@ -30,8 +30,6 @@ class UniversityDriViewSet(BasicModuleViewSet):
queryset = UniversityDri.objects.all() # pylint: disable=E1101
serializer_class = UniversityDriSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
univ_id = self.kwargs["univ_id"]
return self.my_model_queryset.filter(universities__pk=univ_id).distinct()
......@@ -32,8 +32,6 @@ class UniversityScholarshipViewSet(ScholarshipViewSet):
queryset = UniversityScholarship.objects.all() # pylint: disable=E1101
serializer_class = UniversityScholarshipSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
univ_id = self.kwargs["univ_id"]
return self.my_model_queryset.filter(universities__pk=univ_id).distinct()
......@@ -33,8 +33,6 @@ class UniversityTaggedItemViewSet(TaggedItemViewSet):
queryset = UniversityTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = UniversityTaggedItemSerializer
BYPASS_DICT_MODE = True
def extend_queryset(self):
univ_id = self.kwargs["univ_id"]
return self.my_model_queryset.filter(university__pk=univ_id).distinct()
......@@ -68,7 +68,7 @@ WEBPACK_LOADER = {
STATICFILES_DIRS = (
os.path.join(
BASE_DIR, "static/frontend_app/bundles/"
BASE_DIR, "backend/static/frontend_app/bundles/"
), # We do this so that django's collectstatic copies or our bundles to the STATIC_ROOT or syncs them to whatever storage we use.
)
# End of webpack loader related
......
#OutGoing_REX
Django==2.0.10
psycopg2-binary==2.7.4
Django==2.1.7
psycopg2-binary==2.7.7
django-cas-ng==3.6.0
djangorestframework==3.8.2 # Django REST Framework
djangorestframework==3.9.1
coreapi==2.3.3 # Automatic API doc generation
django-reversion==2.0.13
django-reversion-compare==0.8.4
django-reversion==3.0.3
django-reversion-compare==0.8.6
pytest-django==3.4.7
pytest-cov==2.6.1
pytest-xdist==1.23.0
black
flake8
django-debug-toolbar==1.9.1
pandas
pyyaml
django-debug-toolbar==1.11
pandas==0.24.1
pyyaml==3.13
django-extensions==2.1.5
uwsgi
dotmap
uwsgi==2.0.18
dotmap==1.3.4
django-webpack-loader==0.6.0
ipython # For a better Django shell
# Direct dev depencies
ipython==7.3.0 # For a better Django shell
black==18.9b0
flake8==3.7.6
......@@ -30,7 +30,7 @@ services:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
# Run the django developpement server on image startup.
command: /bin/sh -c "cd backend && ./manage.py runserver 0.0.0.0:8000"
command: /bin/sh -c "cd backend && ./manage.py collectstatic --noinput && ./manage.py runserver 0.0.0.0:8000"
depends_on:
# Required that the `database` service is up and running.
- database
......@@ -58,6 +58,8 @@ services:
ports:
# Replicate the node server port. More info in ./frontend/server.js
- 3000:3000
# replicate the view stats port
- "8888:8888"
documentation:
build: ./documentation
volumes:
......
{
"presets": [
"env",
"react",
"es2015",
"stage-1"
"@babel/preset-env",
"@babel/preset-react",
],
"plugins": [
"transform-class-properties",
"react-hot-loader/babel"
]
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties",
],
}
......@@ -35,8 +35,8 @@ module.exports = {
"error",
"always"
],
"react/no-unescaped-entities": "warn",
"react/prop-types": "warn",
"react/no-unescaped-entities": "off", // that one doesn't improve code readability
"react/prop-types": "error",
"react/no-deprecated": "warn"
}
};
node_modules
webpack-stats.json
stats.json
This diff is collapsed.
......@@ -9,8 +9,8 @@
"dev": "node server.js",
"build": "webpack --config webpack.config.build.js",
"test": "echo \"Error: no test specified\" && exit 1",
"stats": "webpack --profile --json > frontend/static/frontend/stats.json",
"view-stats": "webpack-bundle-analyzer frontend/static/frontend/stats.json"
"stats": "webpack --config webpack.config.build.js --profile --json > stats.json",
"view-stats": "webpack-bundle-analyzer --host 0.0.0.0 --port 8888 stats.json"
},
"repository": {
"type": "git",
......@@ -19,60 +19,59 @@
"author": "",
"license": "ISC",
"dependencies": {
"@material-ui/core": "^3.1.0",
"@material-ui/icons": "^2.0.3",
"@material-ui/lab": "^3.0.0-alpha.16",
"babel-preset-stage-0": "^6.24.1",
"date-fns": "^2.0.0-alpha.16",
"downshift": "^2.2.0",
"@babel/core": "^7.0.0",
"@date-io/date-fns": "^1.1.0",
"@date-io/luxon": "^1.1.0",
"@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.2",
"@material-ui/lab": "^3.0.0-alpha.30",
"date-fns": "^2.0.0-alpha.25",
"downshift": "^3.2.3",
"fuzzysort": "^1.1.4",
"js-cookie": "^2.2.0",
"leaflet": "^1.3.4",
"leaflet": "^1.4.0",
"lodash": "^4.17.11",
"material-ui-pickers": "^1.0.0-rc.14",
"react": "^16.5.1",
"material-ui-pickers": "^2.2.1",
"react": "^16.8.2",
"react-awesome-slider": "^0.5.2",
"react-dom": "^16.5.1",
"react-leaflet": "^2.0.1",
"react-markdown": "^3.6.0",
"react-redux": "^5.0.7",
"react-dom": "^16.8.2",
"react-leaflet": "^2.2.1",
"react-markdown": "^4.0.6",
"react-redux": "^6.0.1",
"react-router-dom": "^4.3.1",
"react-swipeable-views": "^0.12.17",
"redux": "^4.0.0",
"react-swipeable-views": "^0.13.1",
"redux": "^4.0.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"typeface-roboto": "0.0.54"
},
"devDependencies": {
"autoprefixer": "^9.1.5",
"babel-core": "^6.26.3",
"@babel/core": "^7.3.3",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"autoprefixer": "^9.4.8",
"babel-eslint": "^10.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-1": "^6.24.1",
"css-loader": "^1.0.0",
"eslint": "^5.13.0",
"eslint-config-google": "^0.9.1",
"babel-loader": "^8.0.5",
"css-loader": "^2.1.0",
"eslint": "^5.14.1",
"eslint-plugin-react": "^7.12.4",
"file-loader": "^2.0.0",
"file-loader": "^3.0.1",
"js-yaml-loader": "^1.0.1",
"mini-css-extract-plugin": "^0.4.2",
"node-sass": "^4.9.3",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0",
"postcss-loader": "^3.0.0",
"prop-types": "^15.6.2",
"react-hot-loader": "^4.6.5",
"prop-types": "^15.7.2",
"react-hot-loader": "^4.7.1",
"react-lorem-component": "^0.13.0",
"react-script": "^2.0.5",
"sass-loader": "^7.1.0",
"webpack": "^4.17.0",
"webpack-bundle-analyzer": "^3.0.2",
"webpack": "^4.29.5",
"webpack-bundle-analyzer": "^3.0.4",
"webpack-bundle-tracker": "^0.4.2-beta",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.14",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.0",
"webpack-merge": "^4.2.1"
}
}
......@@ -71,7 +71,7 @@ export default class CrudReducers {
return is(state, action, self.types.isReadingAll);
}
function readAllSucceeded(state = { data: Object(), readAt: 0 }, action) {
function readAllSucceeded(state = { data: Array(), readAt: 0 }, action) {
return succeeded(state, action, self.types.readAllSucceeded);
}
......
......@@ -186,7 +186,7 @@ App.propTypes = {
classes: PropTypes.object.isRequired,
};
// Already load some of the data even if it's not use here.
const mapStateToProps = (state) => {
return {
countries: state.api.countriesAll,
......
import React, { Component } from "react";
import Loading from "./other/Loading";
import PropTypes from "prop-types";
// Stores the name of the reducers/actions that result in read data
const successActionsWithReads = ["readSucceeded", "createSucceeded", "updateSucceeded"];
......@@ -222,36 +224,71 @@ class CustomComponentForAPI extends Component {
return out;
}
/**
* Access to the read data from the propNames array
* This should be used instead of getAllReadData to optimize things
*
* @param {array} propNames Array of the prop names you want to read data from
* @returns {object}
* @memberof CustomComponentForAPI
*/
getReadDataFor(propNames) {
let out = Object();
propNames.forEach(propName => out[propName] = this.getReadData(propName));
return out;
}
// utilities functions shared by subclasses
/**
* Function to retrieve all the information relative to a campus
*
* @param {object} campus instance of Campus
* @returns {object} Campus with replaced university, city and country with the matching instance
* @memberof CustomComponentForAPI
*/
joinCampus(campus) {
const { universities, countries, cities } = this.getAllReadData();
const { universities, countries, cities } = this.getReadDataFor(["universities", "countries", "cities"]);
let res = Object.assign({}, campus); //copy for safety
res.university = universities[campus.university];
res.city = cities[campus.city];
res.country = countries[res.city.country];
res.university = universities.find(univ => univ.id = campus.university);
res.city = cities.find(city => city.id = campus.city);
res.country = countries.find(country => country.id = res.city.country);
return res;
}
/**
* Funciton to get the city and the country of a university given a university id
*
* @param {number} univId
* @returns {object} Object with city and country instance of the university (main campus)
* @memberof CustomComponentForAPI
*/
getUnivCityAndCountry(univId) {
const univMainCampus = this.findMainCampus(univId);
const cities = this.getReadData("cities");
const countries = this.getReadData("countries");
const city = cities[univMainCampus.city];
const country = countries[city.country];
const univMainCampus = this.findMainCampus(univId),
{ countries, cities } = this.getReadDataFor(["countries", "cities"]),
city = cities.find(city => city.id == univMainCampus.city),
country = countries.find(country => country.id == city.country);
return { city, country };
}
/**
* Function that returns the main campus instance associated with a university
* identified by its id.
*
* @param {number} univId
* @returns {object}
* @memberof CustomComponentForAPI
*/
findMainCampus(univId) {
const mainCampuses = this.getReadData("mainCampuses");
for (let mainCampusPk in mainCampuses) {
const campus = mainCampuses[mainCampusPk];
if (campus.university == univId) {
return campus;
}
}
return null;
return mainCampuses.find(campus => campus.university == univId);
}
}
CustomComponentForAPI.propTypes = {
api: PropTypes.object
};
export default CustomComponentForAPI;
......@@ -49,13 +49,15 @@ class ThemeProvider extends CustomComponentForAPI {
customRender() {
const font = {
const siteSettings = {
typography: {
fontSize: 14,
htmlFontSize: 14
}
},
useNextVariants: true
};
const theme = Object.assign({}, this.state.theme, font);
const theme = Object.assign({}, this.state.theme, siteSettings);
return (
<div>
<MuiThemeProvider theme={createMuiTheme(theme)}>
......
......@@ -84,10 +84,10 @@ class DownshiftMultiple extends React.Component {
const { selectedItems } = this.state;
let possible = __difference(options, selectedItems);
const filter = fuzzysort.go(value, possible, { limit: 5, key: "label" });
if (filter.length > 0){
if (filter.length > 0) {
return __map(filter, (item) => item.obj);
} else {
return possible.slice(0,4);
return possible.slice(0, 4);
}
}
......@@ -142,14 +142,16 @@ class DownshiftMultiple extends React.Component {
onChange={this.handleChange}
selectedItem={selectedItems}
>
{({
{
({
getInputProps,
getItemProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
}) => (
}) =>
(
<div className={classes.container}>
{renderInput({
fullWidth: true,
......@@ -186,7 +188,8 @@ class DownshiftMultiple extends React.Component {
</Paper>
) : null}
</div>
)}
)
}
</Downshift>
</div>
......@@ -196,6 +199,12 @@ class DownshiftMultiple extends React.Component {
DownshiftMultiple.propTypes = {
classes: PropTypes.object.isRequired,
onComponentUnmount: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
config: PropTypes.object.isRequired,
field_label: PropTypes.string.isRequired,
field_placeholder: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
};
DownshiftMultiple.defaultProps = {
......
......@@ -3,8 +3,6 @@ import DownshiftMultiple from "./DownshiftMultiple";
import CustomComponentForAPI from "../CustomComponentForAPI";
import { connect } from "react-redux";
import __each from "lodash/each";
import __uniq from "lodash/uniq";
import __map from "lodash/map";
import __indexOf from "lodash/indexOf";
import { saveSelectedUniversities, saveFilterConfig } from "../../actions/filter";
......@@ -31,34 +29,44 @@ const styles = theme => ({
class Filter extends CustomComponentForAPI {
saveContriesFilterConfig(state) {
this.props.saveConfig({ contriesFilter: state });
saveCountriesFilterConfig(state) {
this.props.saveConfig({ countriesFilter: state });
}
/**
* Function to get the list of countries where there are universities.
*
* @returns {Array} of the countries instances
* @memberof Filter
*/
getCountriesWhereThereAreUniversities() {
const { mainCampuses } = this.getAllReadData();
const mainCampuses = this.getReadData("mainCampuses");
let res = [];
__each(mainCampuses, (campus) => {
const campusFull = this.joinCampus(campus);
res.push(campusFull.country);
// use of map to get only each country once
let res = new Map();
// TODO optimize: use maps instead of joinCampus
mainCampuses.forEach(campus => {
const country = this.joinCampus(campus).country,
code = country.iso_alpha2_code;
res.set(code, country);
});
return __uniq(res, false, (c) => { return c.iso_alpha2_code; });
return [...res.values()];
}
updateSelectedUniversities(selection) {
const { mainCampuses } = this.getAllReadData();
const listOfCountries = __map(selection, (s) => s.id);
const mainCampuses = this.getReadData("mainCampuses"),
listOfCountries = __map(selection, (s) => s.id);
let selected_universities = [];
__each(mainCampuses, (campus) => {
let selectedUniversities = [];
mainCampuses.forEach(campus => {
const campusFull = this.joinCampus(campus);
if (__indexOf(listOfCountries, campusFull.country.iso_alpha2_code) > -1) {
selected_universities.push(campusFull.university.id);
selectedUniversities.push(campusFull.university.id);
}
});
this.props.saveSelection(selected_universities);
this.props.saveSelection(selectedUniversities);
}
customRender() {
......@@ -74,8 +82,8 @@ class Filter extends CustomComponentForAPI {
<DownshiftMultiple
options={options}
onChange={(selection) => this.updateSelectedUniversities(selection)}
onComponentUnmount={(state) => this.saveContriesFilterConfig(state)}
config={this.props.contriesFilterConfig}
onComponentUnmount={(state) => this.saveCountriesFilterConfig(state)}
config={this.props.countriesFilterConfig}
/>
</ExpansionPanelDetails>
</ExpansionPanel>
......@@ -91,7 +99,7 @@ const mapStateToProps = (state) => {
mainCampuses: state.api.mainCampusesAll,
cities: state.api.citiesAll,
countries: state.api.countriesAll,
contriesFilterConfig: state.app.filter.contriesFilter
countriesFilterConfig: state.app.filter.countriesFilter
};
};
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import CardMedia from "@material-ui/core/CardMedia";
class MyCardMedia extends Component {
......@@ -6,7 +7,7 @@ class MyCardMedia extends Component {
render() {
const { title, height, url } = this.props;
if (url == "") {
return (<div/>);
return (<div />);
}
return (
<CardMedia
......@@ -20,5 +21,10 @@ class MyCardMedia extends Component {
}
}
MyCardMedia.propTypes = {
title: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};
export default MyCardMedia;
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Marker, Popup } from "react-leaflet";
// import MarkerClusterGroup from 'react-leaflet-markercluster';
......@@ -7,44 +8,61 @@ import UnivPopupContent from "./UnivPopupContent";
import getActions from "../../api/getActions";