Commit a065c242 authored by Florent Chehab's avatar Florent Chehab

Merge branch 'post_data' into 'master'

Post data and color work

See merge request chehabfl/outgoing_rex!32
parents 05513735 c5398c92
Pipeline #26975 passed with stages
in 2 minutes and 49 seconds
......@@ -33,6 +33,10 @@ class MyModelSerializer(MySerializerWithJSON):
pending_moderation = serializers.SerializerMethodField()
model_config = serializers.SerializerMethodField()
# For easier handling on the client side, we force an id field
# this is usefull when a model has a dedicated primary key
id = serializers.SerializerMethodField()
def get_model_config(self, obj=None):
return self.Meta.model.model_config
......@@ -41,6 +45,9 @@ class MyModelSerializer(MySerializerWithJSON):
return PendingModerationSerializer(obj.pending_moderation, many=True, read_only=True, context=self.context).data
return None
def get_id(self, obj):
return obj.pk
class Meta:
model = MyModel
......
......@@ -6,12 +6,14 @@ 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
def list(self, request, *args, **kwargs):
response = super(viewsets.ModelViewSet, self).list( # pylint: disable=E1003
request, *args, **kwargs) # call the original 'list'
pk_attr_name = self.serializer_class.Meta.model._meta.pk.name
response.data = {
d[pk_attr_name]: d for d in response.data
}
if 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
......@@ -23,11 +23,10 @@ class UserDataSerializer(MyModelSerializer):
owner = serializers.CharField(read_only=True)
def my_pre_save(self):
user = self.get_user_in_request()
self.override_validated_data({'owner': user})
self.override_validated_data({'owner': self.user})
# we try to recover the correct instance
query = UserData.objects.filter(owner=user)
query = UserData.objects.filter(owner=self.user)
if len(query) == 1:
self.instance = query[0]
......@@ -39,6 +38,7 @@ class UserDataSerializer(MyModelSerializer):
class UserDataViewSet(MyModelViewSet):
permission_classes = get_viewset_permissions("UserDataViewSet")
serializer_class = UserDataSerializer
BYPASS_DICT_MODE = True
def get_queryset(self):
return UserData.objects.filter(owner=self.request.user) # pylint: disable=E1101
......@@ -10,19 +10,19 @@
// {{obj.NAME}}
////////////////
export const {{obj.NAME}}_HAS_ERROR = '{{obj.NAME}}_HAS_ERROR';
export const {{obj.NAME}}_FETCH_HAS_ERROR = '{{obj.NAME}}_FETCH_HAS_ERROR';
export const {{obj.NAME}}_IS_LOADING = '{{obj.NAME}}_IS_LOADING';
export const {{obj.NAME}}_FETCH_DATA_SUCCESS = '{{obj.NAME}}_FETCH_DATA_SUCCESS';
export const {{obj.NAME}}_INVALIDATED = '{{obj.NAME}}_INVALIDATED';
export const {{obj.NAME}}_EL_HAS_ERROR = '{{obj.NAME}}_HAS_ERROR';
export const {{obj.NAME}}_EL_FETCH_HAS_ERROR = '{{obj.NAME}}_EL_FETCH_HAS_ERROR';
export const {{obj.NAME}}_EL_INVALIDATED = '{{obj.NAME}}_EL_INVALIDATED';
export const {{obj.NAME}}_EL_IS_LOADING = '{{obj.NAME}}_IS_LOADING';
export const {{obj.NAME}}_EL_FETCH_DATA_SUCCESS = '{{obj.NAME}}_FETCH_DATA_SUCCESS';
export const {{obj.NAME}}_EL_IS_LOADING = '{{obj.NAME}}_EL_IS_LOADING';
export const {{obj.NAME}}_EL_FETCH_DATA_SUCCESS = '{{obj.NAME}}_EL_FETCH_DATA_SUCCESS';
{% if not obj.read_only %}
export const {{obj.NAME}}_EL_IS_SAVING = '{{obj.NAME}}_IS_POSTING';
export const {{obj.NAME}}_EL_SAVING_DATA_SUCCESS = '{{obj.NAME}}_POST_DATA_SUCCESS';
export const {{obj.NAME}}_EL_IS_SAVING = '{{obj.NAME}}_EL_IS_SAVING';
export const {{obj.NAME}}_EL_SAVING_HAS_ERROR = '{{obj.NAME}}_EL_SAVING_HAS_ERROR';
{% endif %}
{% endfor %}
......@@ -8,18 +8,19 @@ import Cookies from 'js-cookie';
import {
{% for obj in data %}
{{obj.NAME}}_HAS_ERROR,
{{obj.NAME}}_FETCH_HAS_ERROR,
{{obj.NAME}}_IS_LOADING,
{{obj.NAME}}_FETCH_DATA_SUCCESS,
{{obj.NAME}}_INVALIDATED,
{{obj.NAME}}_EL_INVALIDATED,
{{obj.NAME}}_EL_HAS_ERROR,
{{obj.NAME}}_EL_FETCH_HAS_ERROR,
{{obj.NAME}}_EL_IS_LOADING,
{{obj.NAME}}_EL_FETCH_DATA_SUCCESS,
{% if not obj.read_only %}
{{obj.NAME}}_EL_IS_SAVING,
{{obj.NAME}}_EL_SAVING_HAS_ERROR,
{{obj.NAME}}_EL_SAVING_DATA_SUCCESS,
{% endif %}
......@@ -30,9 +31,12 @@ import {
// generic function definitions
function _FetchData(pk, api_end_point, _IsLoading, _FetchDataSuccess, _Invalidated, _HasError, pk_required=false) {
if (pk_required && pk == ""){
if (pk_required && (typeof pk == 'undefined')){
throw "pk shouldn't be empty when requesting a specific element";
}
if (pk != ""){
api_end_point += pk;
}
return (dispatch) => {
dispatch(_IsLoading(true));
let token = Cookies.get('csrftoken');
......@@ -49,19 +53,22 @@ function _FetchData(pk, api_end_point, _IsLoading, _FetchDataSuccess, _Invalidat
dispatch(_FetchDataSuccess(obj));
dispatch(_IsLoading(false));
})
.catch(() => dispatch(_HasError(true)));
.catch((e) => {
dispatch(_HasError(true, e));
dispatch(_IsLoading(false));
});
};
}
function _ElSaveData(data, api_end_point, _IsLoading, _FetchDataSuccess, _Invalidated, _HasError) {
function _ElSaveData(data, api_end_point, _ElIsSaving, _ElFetchDataSuccess, _ElInvalidated, _ElHasError) {
return (dispatch) => {
let method = "POST";
let pk = "";
if ('id' in data){
method = "PUT";
pk = data.id;
pk = data.id + '/';
}
dispatch(_ElIsSaving(true));
......@@ -86,20 +93,24 @@ function _ElSaveData(data, api_end_point, _IsLoading, _FetchDataSuccess, _Invali
.then((response) => response.json())
.then((_El) => {
dispatch(_ElInvalidated(false));
dispatch(_ElSaveDataSuccess(_El));
dispatch(_ElFetchDataSuccess(_El)); // we use the same here
dispatch(_ElIsSaving(false));
})
.catch(() => dispatch(_ElHasError(true)));
.catch((e) => {
dispatch(_ElHasError(true, e))
dispatch(_ElIsSaving(false));
});
};
}
/////////////////////////////////
{% for obj in data %}
export function {{obj.name}}HasError(bool) {
export function {{obj.name}}FetchHasError(bool, error=null) {
return {
type: {{obj.NAME}}_HAS_ERROR,
hasError: bool
type: {{obj.NAME}}_FETCH_HAS_ERROR,
hasError: bool,
error
};
}
......@@ -135,15 +146,16 @@ export function {{obj.name}}FetchData() {
{{obj.name}}IsLoading,
{{obj.name}}FetchDataSuccess,
{{obj.name}}Invalidated,
{{obj.name}}HasError
{{obj.name}}FetchHasError
)
}
export function {{obj.name}}ElHasError(bool) {
export function {{obj.name}}ElFetchHasError(bool, error) {
return {
type: {{obj.NAME}}_EL_HAS_ERROR,
hasError: bool
type: {{obj.NAME}}_EL_FETCH_HAS_ERROR,
hasError: bool,
error
};
}
......@@ -180,7 +192,7 @@ export function {{obj.name}}ElFetchData(pk) {
{{obj.name}}ElIsLoading,
{{obj.name}}ElFetchDataSuccess,
{{obj.name}}ElInvalidated,
{{obj.name}}ElHasError,
{{obj.name}}ElFetchHasError,
true
)
}
......@@ -195,19 +207,17 @@ export function {{obj.name}}ElIsSaving(bool) {
};
}
export function {{obj.name}}ElSaveDataSuccess({{obj.name}}El) {
{{obj.name}}ElInvalidated(false)
export function {{obj.name}}ElSavingHasError(bool, error=null) {
return {
type: {{obj.NAME}}_EL_SAVING_DATA_SUCCESS,
{{obj.name}}El,
{{obj.name}}ElSavedAt: Date.now()
type: {{obj.NAME}}_EL_SAVING_HAS_ERROR,
hasError: bool,
error
};
}
export function {{obj.name}}ElSaveData(data) {
return _ElSaveData(data, "{{obj.api_end_point}}", {{obj.name}}ElIsLoading, {{obj.name}}ElFetchDataSuccess, {{obj.name}}ElInvalidated, {{obj.name}}ElHasError)
return _ElSaveData(data, "{{obj.api_end_point}}", {{obj.name}}ElIsSaving, {{obj.name}}ElFetchDataSuccess, {{obj.name}}ElInvalidated, {{obj.name}}ElSavingHasError)
}
{% endif %}
......
......@@ -9,18 +9,18 @@ import { combineReducers } from 'redux';
import {
{% for obj in data %}
{{obj.name}}Invalidated,
{{obj.name}}HasError,
{{obj.name}}FetchHasError,
{{obj.name}}IsLoading,
{{obj.name}}Fetched,
{{obj.name}}ElInvalidated,
{{obj.name}}ElHasError,
{{obj.name}}ElFetchHasError,
{{obj.name}}ElIsLoading,
{{obj.name}}ElFetched,
{% if not obj.read_only %}
{{obj.name}}ElIsSaving,
{{obj.name}}ElSaved,
{{obj.name}}ElSavingHasError,
{% endif %}
{% endfor %}
......@@ -29,25 +29,21 @@ import {
{% for obj in data %}
export const {{obj.name}}Reducers = combineReducers({
invalidated: {{obj.name}}Invalidated,
hasError: {{obj.name}}HasError,
fetchHasError: {{obj.name}}FetchHasError,
isLoading: {{obj.name}}IsLoading,
fetched: {{obj.name}}Fetched,
})
export const {{obj.name}}ElReducers = combineReducers({
invalidated: {{obj.name}}ElInvalidated,
hasError: {{obj.name}}ElHasError,
fetchHasError: {{obj.name}}ElFetchHasError,
isLoading: {{obj.name}}ElIsLoading,
fetched: {{obj.name}}ElFetched,
{% if not obj.read_only %}
isSaving: {{obj.name}}ElIsSaving,
isSaved: {{obj.name}}ElSaved,
savingHasError: {{obj.name}}ElSavingHasError
{% endif %}
})
{% endfor %}
......@@ -7,17 +7,18 @@
import {
{% for obj in data %}
{{obj.NAME}}_INVALIDATED,
{{obj.NAME}}_HAS_ERROR,
{{obj.NAME}}_FETCH_HAS_ERROR,
{{obj.NAME}}_IS_LOADING,
{{obj.NAME}}_FETCH_DATA_SUCCESS,
{{obj.NAME}}_EL_INVALIDATED,
{{obj.NAME}}_EL_HAS_ERROR,
{{obj.NAME}}_EL_FETCH_HAS_ERROR,
{{obj.NAME}}_EL_IS_LOADING,
{{obj.NAME}}_EL_FETCH_DATA_SUCCESS,
{% if not obj.read_only %}
{{obj.NAME}}_EL_IS_SAVING,
{{obj.NAME}}_EL_SAVING_HAS_ERROR,
{{obj.NAME}}_EL_SAVING_DATA_SUCCESS,
{% endif %}
......@@ -25,10 +26,13 @@ import {
} from "./action-types";
{% for obj in data %}
export function {{obj.name}}HasError(state = false, action) {
export function {{obj.name}}FetchHasError(state = {status: false, error: null}, action) {
switch (action.type) {
case {{obj.NAME}}_HAS_ERROR:
return action.hasError;
case {{obj.NAME}}_FETCH_HAS_ERROR:
return {
status: action.hasError,
error: action.error
}
default:
return state;
......@@ -80,11 +84,13 @@ export function {{obj.name}}Fetched(state = { data: Object(), fetchedAt: null },
export function {{obj.name}}ElHasError(state = false, action) {
export function {{obj.name}}ElFetchHasError(state = {status: false, error: null}, action) {
switch (action.type) {
case {{obj.NAME}}_EL_HAS_ERROR:
return action.hasError;
case {{obj.NAME}}_EL_FETCH_HAS_ERROR:
return {
status: action.hasError,
error: action.error
}
default:
return state;
}
......@@ -121,25 +127,27 @@ export function {{obj.name}}ElFetched(state = { data: Object(), fetchedAt: null
export function {{obj.name}}ElIsSaving(state = false, action) {
switch (action.type) {
case {{obj.NAME}}_EL_IS_SAVING:
return action.isPosting;
return action.isSaving;
default:
return state;
}
}
export function {{obj.name}}ElSaved(state = { data: Object(), savedAt: null }, action) {
export function {{obj.name}}ElSavingHasError(state = {status: false, error: null}, action) {
switch (action.type) {
case {{obj.NAME}}_EL_SAVING_DATA_SUCCESS:
case {{obj.NAME}}_EL_SAVING_HAS_ERROR:
return {
data: action.{{obj.name}}El,
fetchedAt: action.{{obj.name}}ElSavedAt
status: action.hasError,
error: action.error
}
default:
return state;
}
}
{% endif %}
{% endfor %}
\ No newline at end of file
......@@ -108,6 +108,7 @@ class App extends MyComponent {
open: true,
};
handleDrawerOpen = () => {
this.setState({ open: true });
};
......@@ -178,7 +179,7 @@ App.propTypes = {
const mapStateToProps = (state) => {
return {
countries: state.countries,
currencies: state.currencies
currencies: state.currencies,
}
};
......@@ -187,7 +188,7 @@ const mapDispatchToProps = (dispatch) => {
fetchData: {
countries: () => dispatch(countriesFetchData()),
currencies: () => dispatch(currenciesFetchData()),
}
},
};
};
......
......@@ -2,90 +2,169 @@ import React, { Component } from 'react';
import Loading from './other/Loading';
class MyComponent extends Component {
getFetchedData(prop) {
return this.props[prop].fetched.data;
customErrorHandlers = {}
requireUserData = false;
// If true,
// You need to import
// userDataElFetchData,
// userDataElSaveData,
//your mapStateToProps should contain:
// const mapStateToProps = (state) => {
// return {
// userDataEl: state.userDataEl
// }
// };
//
// And your mapDispatchToProps should have:
// const mapDispatchToProps = (dispatch) => {
// return {
// fetchUserDataSingle: (id) => dispatch(userDataElFetchData(id)),
// fetchData: {
// userDataEl: () => dispatch(userDataElFetchData("")),
// },
// createUserData: () => dispatch(userDataElSaveData(Object()))
// };
// };
getFetchedData(prop) {
return this.props[prop].fetched.data;
}
getAllFetchedData() {
let out = Object()
const { props } = this;
for (let prop_key in props) {
let prop = props[prop_key];
if (typeof prop == 'boolean') {
continue;
}
if (prop && 'fetched' in prop) {
out[prop_key] = prop.fetched.data;
}
}
getAllFetchedData() {
let out = Object()
for (let prop_key in this.props) {
let prop = this.props[prop_key];
if (prop && 'fetched' in prop) {
out[prop_key] = prop.fetched.data;
}
}
return out;
return out;
}
joinCampus(campus) {
const { universities, countries, cities } = this.getAllFetchedData();
let res = Object.assign({}, campus); //copy for safety
res.university = universities[campus.university];
res.city = cities[campus.city]
res.country = countries[res.city.country]
return res;
}
checkProps(val) {
const { props } = this;
for (let el in props) {
let prop = props[el];
if (typeof prop == 'boolean') {
continue;
}
if (prop && val in prop && prop[val]) {
return true;
}
}
joinCampus(campus) {
const { universities, countries, cities } = this.getAllFetchedData();
let res = Object.assign({}, campus); //copy for safety
res.university = universities[campus.university];
res.city = cities[campus.city]
res.country = countries[res.city.country]
return res;
return false;
}
checkPropsHasError() {
const { props } = this;
for (let prop_key in props) {
let prop = props[prop_key];
if (typeof prop == 'boolean') {
continue;
}
checkProps(val) {
for (let el in this.props) {
let prop = this.props[el];
if (prop && val in prop && prop[val]) {
return true;
}
if (prop && 'fetchHasError' in prop) {
if (prop.fetchHasError.status) {
if (prop_key in this.customErrorHandlers) {
console.log("icicicicici");
} else {
return true;
}
}
return false;
}
}
return false;
}
checkPropsHasError() {
return this.checkProps('hasError');
}
checkPropsIsLoading() {
return this.checkProps('isLoading');
}
checkPropsIsLoading() {
return this.checkProps('isLoading');
}
checkPropsInvalidated() {
return this.checkProps('invalidated');
}
checkPropsInvalidated() {
return this.checkProps('invalidated');
}
checkPropsIsSaving() {
return this.checkProps('isSaving');
}
loadPropsIfNeeded() {
for (let prop_key in this.props) {
let prop = this.props[prop_key];
if (prop && 'fetched' in prop) {
if ((!prop.fetched.fetchedAt) || prop.invalidated) {
if (!prop.isLoading) {
this.props.fetchData[prop_key]();
}
}
}
loadPropsIfNeeded() {
if (this.checkPropsHasError()) {
return false;
}
const { props } = this;
for (let prop_key in props) {
let prop = props[prop_key];
if (typeof prop == 'boolean') {
continue;
}
if (prop && 'fetched' in prop) {
if ((!prop.fetched.fetchedAt) || prop.invalidated) {
if (!prop.isLoading) {
props.fetchData[prop_key]();
}
}
}
}
componentDidMount() {
this.loadPropsIfNeeded();
this.myComponentDidMount();
}
componentDidMount() {
this.loadPropsIfNeeded();
this.myComponentDidMount();
}
myComponentDidMount() { };
componentDidUpdate() {
// TODO ajouter expire date
this.loadPropsIfNeeded();
if (this.requireUserData) {
//TODO cleaner too many loads
// load the correct user data
const { userDataEl } = this.props;
if (userDataEl.isLoading || userDataEl.isSaving) {
return;
}
const userData = this.getFetchedData("userDataEl");
if (userData instanceof Array) {
if (userData.length == 0) {
if (!this.props.userDataEl.isSaving) {
this.props.createUserData();
}
} else if (userData.length == 1) {
this.props.fetchUserDataSingle(userData[0].id)
} else {
throw Error("We shouldn't have more thant one here !")
}
}
}
myComponentDidMount() { };
this.myComponentDidUpdate();
}
myComponentDidUpdate() { };
componentDidUpdate() {
// TODO ajouter expire date
this.loadPropsIfNeeded();
this.myComponentDidMount();
render() {
if (this.checkPropsHasError()) {
return <p>Sorry! There was an error loading the items</p>;
}
myComponentDidMount() { };
render() {
if (this.checkPropsHasError()) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.checkPropsIsLoading() || this.checkPropsInvalidated()) {
return <Loading />;
}
return this.myRender();
if (this.checkPropsIsLoading() || this.checkPropsInvalidated() || this.checkPropsIsSaving()) {
return <Loading />;
}
return this.myRender();
}
}
......
......@@ -4,16 +4,49 @@ import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import { connect } from "react-redux";
import { BrowserRouter as Router, Route } from 'react-router-dom';
import MyComponent from './MyComponent';
import areSameThemes from '../utils/areSameThemes';
import {
userDataElSaveData,
userDataElFetchData
} from '../generated/actions';
import { saveAppTheme } from '../actions/theme';
class ThemeProvider extends React.Component {
render() {
console.log("ici")
class ThemeProvider extends MyComponent {
requireUserData = true;
state = { theme: this.props.themeSavedInTheApp.theme };
myComponentDidUpdate() {
const currentTheme = this.state.theme;
const themeSavedInTheApp = this.props.themeSavedInTheApp.theme;
const themeSavedInTheAppAt = this.props.themeSavedInTheApp.savedAt;
let themeOnServer = Object();
let themeOnServerRetrievedAt = 0;
try {
themeOnServer = this.props.userDataEl.fetched.data.config.theme;
themeOnServerRetrievedAt = this.props.userDataEl.fetched.fetchedAt;
} catch (e) {
if (!e instanceof TypeError) {
throw e;
}
}
const themeToRender = themeSavedInTheAppAt > themeOnServerRetrievedAt ? themeSavedInTheApp : themeOnServer;
if ((!areSameThemes(themeToRender, currentTheme)) && typeof themeToRender != 'undefined') {
this.setState({ theme: themeToRender });
if (!areSameThemes(themeToRender, themeSavedInTheApp)){
this.props.saveTheme(themeToRender);
}
}
}
myRender() {
return (
<div>
<MuiThemeProvider theme={createMuiTheme(this.props.theme)}>
<MuiThemeProvider theme={createMuiTheme(this.state.theme)}>
<Router>
{this.props.children}
</Router>
......@@ -25,9 +58,20 @@ class ThemeProvider extends React.Component {
const mapStateToProps = (state) => {
return {
theme: state.app.appTheme
themeSavedInTheApp: state.app.appTheme,
userDataEl: state.userDataEl
};
};
const mapDispatchToProps = (dispatch) => {
return {
saveTheme: (theme) => dispatch(saveAppTheme(theme)),
fetchUserDataSingle: (id) => dispatch(userDataElFetchData(id)),
fetchData: {
userDataEl: () => dispatch(userDataElFetchData("")),
},
createUserData: () => dispatch(userDataElSaveData(Object()))
};
};
export default connect(mapStateToProps)(ThemeProvider);
export default connect(mapStateToProps, mapDispatchToProps)(ThemeProvider);