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

Merge branch 'update_to_the_editor' into 'master'

Editor / Forms / Modules / etc.  and complete frontend file reorganization

Closes #22 and #69

See merge request !57
parents ff8b40ab 5749fb2f
Pipeline #35811 canceled with stages
......@@ -15,6 +15,8 @@ CLASSIC_MODELS = []
# Go through the API configuraion
for entry in api_config:
if "is_api_view" in entry and entry["is_api_view"]:
continue
if "model" in entry and entry["model"]:
model_obj = DotMap(entry)
if (not model_obj.requires_testing) and (not model_obj.ignore_in_admin):
......
from rest_framework.views import APIView
from rest_framework.response import Response
from backend_app.utils import get_viewset_permissions
from django.conf import settings
from django.http import HttpResponse
import json
from shared import OBJ_MODERATION_PERMISSIONS
def app_moderation_status(request):
return HttpResponse(
json.dumps(
class AppModerationStatusViewSet(APIView):
"""
"""
permission_classes = get_viewset_permissions("AppModerationStatusViewSet")
def get(self, request):
return Response(
{
"activated": settings.MODERATION_ACTIVATED,
"moderator_level": OBJ_MODERATION_PERMISSIONS["moderator"],
}
)
)
......@@ -10,6 +10,8 @@ ALL_VIEWSETS = {}
for model in api_config:
model = DotMap(model)
if "is_api_view" in model and model.is_api_view:
continue
if not model.requires_testing:
if model.viewset != "UserDataViewSet":
module = importlib.import_module(
......@@ -20,6 +22,8 @@ for model in api_config:
if settings.TESTING:
for model in api_config:
model = DotMap(model)
if "is_api_view" in model and model.is_api_view:
continue
if model.requires_testing:
if model.viewset != "UserDataViewSet":
module = importlib.import_module(
......
from django.conf import settings
from django.conf.urls import include, url
from django.urls import path
from rest_framework import routers
from rest_framework.documentation import include_docs_urls
from backend_app.permissions import DEFAULT_VIEWSET_PERMISSIONS
from shared import get_api_config
from . import views
from dotmap import DotMap
from .other_viewsets import AppModerationStatusViewSet
import importlib
......@@ -23,6 +22,8 @@ api_config = get_api_config()
for entry in api_config:
model_obj = DotMap(entry)
if "is_api_view" in model_obj and model_obj.is_api_view:
continue
if (not model_obj.requires_testing) or (
settings.TESTING and model_obj.requires_testing
):
......@@ -45,10 +46,10 @@ for entry in api_config:
router.register(str_url, Viewset)
# Add all the endpoints for the base api
urlpatterns += [url(r"^api/", include(router.urls))]
# Add some custom APIs
urlpatterns.append(path("api/serverModerationStatus/", views.app_moderation_status))
urlpatterns += [
url(r"^api/", include(router.urls)),
url(r"^api/serverModerationStatus/", AppModerationStatusViewSet.as_view()),
]
#######
# Models and Viewset checks
......
......@@ -5,6 +5,8 @@ def get_model_config(model):
api_config = get_api_config()
for obj in api_config:
if "is_api_view" in obj and obj["is_api_view"]:
continue
if obj["model"] == model:
tmp = {
"moderation_level": obj["moderation_level"],
......
......@@ -6,7 +6,9 @@ module.exports = {
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
"plugin:react/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"parser": "babel-eslint",
"parserOptions": {
......@@ -19,6 +21,7 @@ module.exports = {
"plugins": [
"react",
"jest",
"import",
],
"rules": {
"indent": [
......@@ -40,6 +43,6 @@ module.exports = {
],
"react/no-unescaped-entities": "off", // that one doesn't improve code readability
"react/prop-types": "error",
"react/no-deprecated": "warn"
"react/no-deprecated": "error"
}
};
This diff is collapsed.
{
"name": "outgoing_rex",
"name": "rex-dri",
"version": "1.0.0",
"description": "[![build](/../badges/master/build.svg)](https://gitlab.utc.fr/chehabfl/outgoing_rex/pipelines) [![coverage](/../badges/master/coverage.svg)](https://chehabfl.gitlab.utc.fr/outgoing_rex/) [![License](https://img.shields.io/badge/License-BSD%202--Clause-green.svg)](https://opensource.org/licenses/BSD-2-Clause)",
"main": "manage.py",
......@@ -33,9 +33,10 @@
"leaflet": "^1.4.0",
"lodash": "^4.17.11",
"material-ui-pickers": "^2.2.1",
"react": "^16.8.2",
"notistack": "^0.4.3",
"react": "^16.8.3",
"react-awesome-slider": "^0.5.2",
"react-dom": "^16.8.2",
"react-dom": "^16.8.3",
"react-leaflet": "^2.2.1",
"react-markdown": "^4.0.6",
"react-redux": "^6.0.1",
......@@ -58,6 +59,8 @@
"babel-loader": "^8.0.5",
"css-loader": "^2.1.0",
"eslint": "^5.14.1",
"eslint-import-resolver-node": "^0.3.2",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.3.0",
"eslint-plugin-react": "^7.12.4",
"file-loader": "^3.0.1",
......
......@@ -16,26 +16,32 @@ import Chip from "@material-ui/core/Chip";
import Avatar from "@material-ui/core/Avatar";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import SchoolIcon from "@material-ui/icons/School";
import { mainListItems, secondaryListItems, thirdListItems } from "./template/listItems";
import { mainListItems, secondaryListItems, thirdListItems } from "./listItems";
import FullScreenDialog from "./FullScreenDialog";
import { connect } from "react-redux";
import CustomComponentForAPI from "./CustomComponentForAPI";
import CustomComponentForAPI from "../common/CustomComponentForAPI";
import {
Route,
Redirect
} from "react-router-dom";
import getActions from "../api/getActions";
import getActions from "../../redux/api/getActions";
import PageMap from "./pages/PageMap";
import PageHome from "./pages/PageHome";
import PageUniversity from "./pages/PageUniversity";
import PageSearch from "./pages/PageSearch";
import PageSettings from "./pages/PageSettings";
import PageMap from "../pages/PageMap";
import PageHome from "../pages/PageHome";
import PageUniversity from "../pages/PageUniversity";
import PageSearch from "../pages/PageSearch";
import PageSettings from "../pages/PageSettings";
const DRAWER_WIDTH = 240;
/**
* @class App
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class App extends CustomComponentForAPI {
state = {
open: true,
......@@ -54,8 +60,7 @@ class App extends CustomComponentForAPI {
const { classes } = this.props;
return (
<React.Fragment>
<>
<CssBaseline />
<div className={classes.root}>
<Drawer
......@@ -106,6 +111,8 @@ class App extends CustomComponentForAPI {
</Drawer>
<FullScreenDialog />
<main className={classNames(classes.content, classes.noPaddingTop)}>
<div className={classes.paddingTop}>
<Route path="/app/" exact={true} component={PageHome} />
......@@ -121,10 +128,11 @@ class App extends CustomComponentForAPI {
<div >
<Route path="/app/university/:id" component={PageUniversity} />
</div>
</main>
</div>
</React.Fragment>
</>
);
}
}
......@@ -138,6 +146,7 @@ const mapStateToProps = (state) => {
return {
countries: state.api.countriesAll,
currencies: state.api.currenciesAll,
serverModerationStatus: state.api.serverModerationStatusSpecific,
};
};
......@@ -146,6 +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
},
};
};
......
import React, { Component } from "react";
import Dialog from "@material-ui/core/Dialog";
import Slide from "@material-ui/core/Slide";
import PropTypes from "prop-types";
import { connect } from "react-redux";
/**
* Component to enable the FullScreenDialog to have nice transitions
* @returns
*/
function Transition(props) {
return <Slide direction="up" {...props} />;
}
/**
* Class to display a full screen dialog.
* It is connected to the redux state to have only one of such dialog across the app.
*
* @class FullScreenDialog
* @extends {Component}
*/
class FullScreenDialog extends Component {
render() {
return (
<Dialog
fullScreen
open={this.props.open}
TransitionComponent={Transition}
>
{this.props.innerNodes}
</Dialog>
);
}
}
FullScreenDialog.propTypes = {
open: PropTypes.bool.isRequired, // Is the full screen dialog open
innerNodes: PropTypes.node.isRequired, // content of the fullScreen Dialog
};
// Get the props from the redux store.
const mapStateToProps = (state) => ({ ...state.app.fullScreenDialog });
export default connect(mapStateToProps)(FullScreenDialog);
/**
* This file contains the general site tempalte
* This file contains elements of the site template
*/
import React from "react";
......@@ -16,7 +16,7 @@ import AssignmentIcon from "@material-ui/icons/Assignment";
import { NavLink } from "react-router-dom";
export const mainListItems = (
<div>
<>
<NavLink to={"/app/"} style={{ textDecoration: "none" }}>
{/* TODO add styling */}
<ListItem button>
......@@ -54,11 +54,11 @@ export const mainListItems = (
</ListItem>
</NavLink>
</div>
</>
);
export const secondaryListItems = (
<div>
<>
<ListItem button>
<ListItemIcon>
<AssignmentIcon />
......@@ -72,12 +72,12 @@ export const secondaryListItems = (
</ListItemIcon>
<ListItemText primary="Informations" />
</ListItem>
</div>
</>
);
export const thirdListItems = (
<div>
<>
<NavLink to={"/app/settings/"} style={{ textDecoration: "none" }}>
<ListItem button>
<ListItemIcon>
......@@ -86,5 +86,5 @@ export const thirdListItems = (
<ListItemText primary="Paramètres" />
</ListItem>
</NavLink>
</div>
</>
);
......@@ -11,53 +11,61 @@ import withStyles from "@material-ui/core/styles/withStyles";
import compose from "recompose/compose";
/**
* Component to render an "alert" that prevents all other interaction on the site.
*
* @class Alert
* @extends {React.Component}
*/
class Alert extends React.Component {
render() {
const { classes } = this.props;
if (!this.props.open) {
return <></>;
}
return (
<Dialog
open={this.props.open}
open={true}
>
{
this.props.open ?
<div>
<DialogTitle>{this.props.title}</DialogTitle>
<DialogContent>
<DialogContentText style={{ whiteSpace: "pre-wrap" }}>
{this.props.description}
</DialogContentText>
</DialogContent>
<DialogActions>
{
this.props.info ?
<Button onClick={() => { this.props.handleClose(); this.props.handleResponse(); }} color="primary">
{this.props.infoText}
<>
<DialogTitle>{this.props.title}</DialogTitle>
<DialogContent>
<DialogContentText style={{ whiteSpace: "pre-wrap" }}>
{this.props.description}
</DialogContentText>
</DialogContent>
<DialogActions>
{
this.props.info ?
<Button onClick={() => { this.props.handleClose(); this.props.handleResponse(); }} color="primary">
{this.props.infoText}
</Button>
:
<div>
<Button
onClick={() => { this.props.handleClose(); this.props.handleResponse(false); }}
color='secondary'
className={this.props.multilineButtons ? classes.multilineButton : classes.button}
>
{this.props.disagreeText}
</Button>
<Button
onClick={() => { this.props.handleClose(); this.props.handleResponse(true); }}
className={this.props.multilineButtons ? classes.multilineButton : classes.button}
variant='outlined'
color="primary"
autoFocus
>
{this.props.agreeText}
</Button>
:
<div>
<Button
onClick={() => { this.props.handleClose(); this.props.handleResponse(false); }}
color='secondary'
className={this.props.multilineButtons ? classes.multilineButton : classes.button}
>
{this.props.disagreeText}
</Button>
<Button
onClick={() => { this.props.handleClose(); this.props.handleResponse(true); }}
className={this.props.multilineButtons ? classes.multilineButton : classes.button}
variant='outlined'
color="primary"
autoFocus
>
{this.props.agreeText}
</Button>
</div>
}
</DialogActions>
</div>
:
<div></div>
</div>
}
</DialogActions>
</>
}
</Dialog >
);
......@@ -66,16 +74,16 @@ class Alert extends React.Component {
Alert.propTypes = {
open: PropTypes.bool.isRequired,
info: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
agreeText: PropTypes.string.isRequired,
disagreeText: PropTypes.string.isRequired,
infoText: PropTypes.string.isRequired,
description: PropTypes.string,
handleClose: PropTypes.func.isRequired,
handleResponse: PropTypes.func.isRequired,
multilineButtons: PropTypes.bool.isRequired,
open: PropTypes.bool.isRequired, // is the alert open
title: PropTypes.string.isRequired, // Title display on the vindow
description: PropTypes.string, // textual description (below the titile)
info: PropTypes.bool.isRequired, // If it's just un info alert with one button
infoText: PropTypes.string.isRequired, // content of the alert when it's an info
agreeText: PropTypes.string.isRequired, // Text display in the "agree" button
disagreeText: PropTypes.string.isRequired, // Text display in the "disagree" button
multilineButtons: PropTypes.bool.isRequired, // should the agree/disagree button displayed on multiple lines
handleClose: PropTypes.func.isRequired, // function called when the alert is closed
handleResponse: PropTypes.func.isRequired, // function called allong the previous one with false if the user disagreed and true otherwise. Or no parameters if info.
classes: PropTypes.object.isRequired
};
......
import React, { Component } from "react";
import Loading from "./other/Loading";
import Loading from "./Loading";
import PropTypes from "prop-types";
// Stores the name of the reducers/actions that result in read data
const successActionsWithReads = ["readSucceeded", "createSucceeded", "updateSucceeded"];
import { successActionsWithReads, getLatestRead } from "../../redux/api/utils";
/**
* Custom react component to be used when called to the api are required to display data of the component.
*
* When extending, you shouldn't use the `render` function but the `customRender` (that will be called once all the data is ready)
*
* Also, when connecting your component to redux, you should use a `mapDispatchToProps` such as this one.
* What is important is that "api" related elements are under the `api` key for optimization purposes.
* NB: if you don't do it this way, the data won't be fetched.
*
```JS
const mapDispatchToProps = (dispatch) => {
return {
api: {
universities: () => dispatch(getActions("universities").readAll()),
},
saveUniversityInView: (univId) => dispatch(saveUniversityBeingViewed(univId))
};
};
```
*
* IMPORTANT: to use some of the function contained in this class, you need to have a matching `mapStateToProps` function for read
* elements: the key of the object should match the one in `api`.
*
* Here `universities` is both in the `api` of `mapDispatchToProps` and in `mapStateToProps`.
```JS
const mapStateToProps = (state) => {
return {
universities: state.api.universitiesAll,
universityBeingViewed: state.app.universityBeingViewed
};
};
```
*
*
* @abstract
* @class CustomComponentForAPI
* @extends {Component}
*/
class CustomComponentForAPI extends Component {
customErrorHandlers = {}
// __apiAttr should be an object
......@@ -13,10 +48,6 @@ class CustomComponentForAPI extends Component {
// mapping should be : props_key => other_props_that contains the attribute to use
__apiAttr = null;
// prevent hard reset of module when rereading data
ignoreInvalidation = false;
constructor(props) {
super(props);
......@@ -59,11 +90,12 @@ class CustomComponentForAPI extends Component {
/**
* Function to use instead of `render`.
*
* @virtual
* @memberof CustomComponentForAPI
*/
customRender() {
// eslint-disable-next-line no-console
console.error("Dev: you forget to define the `customRender` function that is used when rendering within a subClass of CustomComponentForAPI");
throw new Error("Dev: you forget to define the `customRender` function that is used when rendering within a subClass of CustomComponentForAPI");
}
// End of react functions override
......@@ -97,11 +129,11 @@ class CustomComponentForAPI extends Component {
*/
propIsUsable(propName) {
const prop = this.props[propName];
return (this.ignoreInvalidation === true || !prop.isInvalidated)
return (!prop.isInvalidated)
&& successActionsWithReads
.filter(action => action in prop) // general handling of all types of API reducers
.some(action => prop[action].readAt !== 0) // makes sure will consider all success actions
&& ["isReading", "isUpdating", "isCreating"]
&& ["isReading"]//, "isUpdating", "isCreating"] Don't put those in here it may cause unwanted rerendering whole tree when saving
.filter(action => action in prop)
.every(action => prop[action] === false);
}
......@@ -159,23 +191,16 @@ class CustomComponentForAPI extends Component {
}
/**
* Get the data that was read from the api given the `propName` and the time at which is was read
* Get the latest data that was read from the api given the `propName` and the time at which is was read
* read, create and update are taken into account
*
* @param {string} propName
* @returns {Any}
* @memberof CustomComponentForAPI
*/
getReadDataAndTime(propName) {
const prop = this.props[propName];
// Smartly retrieve the latest data
// Stores the name of the reducers/actions that result in read data in this case
const out = successActionsWithReads
.filter(action => action in prop) // general handling of all types of API reducers
.map(action => prop[action])
.reduce(
(prev, curr) => prev.readAt < curr.readAt ? curr : prev,
{ readAt: 0 });
getLatestReadDataAndTime(propName) {
const prop = this.props[propName],
out = getLatestRead(prop);
if (!("data" in out)) {
throw Error(`No read data from the api could be retrieved for: ${propName}`);
......@@ -186,52 +211,71 @@ class CustomComponentForAPI extends Component {
/**
* Get the data that was read from the api given the `propName`
* Get the latest data that was read from the api given the `propName`
* read, create and update are taken into account
*
* @param {string} propName
* @returns {Any}
* @memberof CustomComponentForAPI
*/
getReadData(propName) {
return this.getReadDataAndTime(propName).data;
getLatestReadData(propName) {
return this.getLatestReadDataAndTime(propName).data;
}
/**
* Get the time at which the latest data from the api corresponding to `propName` was read
* In some very rare case, we need to consider only the data that was read and not create ou updated.
* That what this function does.
*
* @param {string} propName
* @returns
* @memberof CustomComponentForAPI
*/
getOnlyReadData(propName) {
return this.props[propName].readSucceeded.data;
}
/**
* Get the latest time at which the latest data from the api corresponding to `propName` was read
*
* read, create and update are taken into account
*
* @param {string} propName
* @returns {Any}
* @memberof CustomComponentForAPI
*/
getReadTime(propName) {
return this.getReadDataAndTime(propName).readAt;
getLatestReadTime(propName) {
return this.getLatestReadDataAndTime(propName).readAt;
}
/**
* Access to all the data loaded from the api given the props
* Access to all the latest data loaded from the api given the props
*
* read, create and update are taken into account
*
* @returns {object}
* @memberof CustomComponentForAPI
*/
getAllReadData() {
getAllLatestReadData() {
let out = Object();
this.apiProps.forEach((propName) => {
out[propName] = this.getReadData(propName);
out[propName] = this.getLatestReadData(propName);
});
return out;
}
/**
* Access to the read data from the propNames array
* This should be used instead of getAllReadData to optimize things
* Access to the latest read data from the propNames array
* This should be used instead of getAllLatestReadData to optimize things