Commit b92d223e authored by Florent Chehab's avatar Florent Chehab
Browse files

Merge branch 'connect_back_front' into 'master'

Connect back front

See merge request chehabfl/outgoing_rex!36
parents a938e15d 6ff038f3
Pipeline #27364 passed with stages
in 3 minutes and 1 second
......@@ -8,7 +8,11 @@ from .tags_config import USEFULL_LINKS_CONFIG
def tagged_item_validation(attrs):
tag_config = attrs["tag"].config
sumbitted_content = attrs["custom_content"]
try:
sumbitted_content = attrs["custom_content"]
except KeyError:
assert len(tag_config.keys()) == 0
return True
validate_content_against_config(tag_config, sumbitted_content)
......@@ -32,7 +36,7 @@ def validate_content_against_config(config, content):
elif field_type == 'photos':
validate_content_against_config({"photos": PHOTOS_TAG_CONFIG}, {
"photos": field_submitted})
elif field_type == 'usefull_links':
elif field_type == 'useful_links':
validate_content_against_config({"ul": USEFULL_LINKS_CONFIG}, {
"ul": field_submitted})
elif field_type == 'array':
......@@ -42,35 +46,3 @@ def validate_content_against_config(config, content):
validate_content_against_config(field_config['content'], item)
else:
raise Exception("Dev, you have implement something here...")
# my_tag_config = {
# "url": {
# "type": "url",
# "required": True,
# "validators": {
# "extension": ["jpg", "jpeg", "png"]
# },
# },
# "title": {
# "type": "text",
# "required": True,
# "validators": {
# "max_length": 200
# }
# },
# "description": {
# "required": False,
# "type": "text",
# "validators": {
# "max_length": 500
# }
# },
# "licence": {
# "type": "text",
# "required": False,
# "validators": {
# "max_length": 200
# }
# }
# }
from .photos import PHOTOS_TAG_CONFIG # noqa: F401
from .usefull_links import USEFULL_LINKS_CONFIG # noqa: F401
from .useful_links import USEFULL_LINKS_CONFIG # noqa: F401
......@@ -5,13 +5,15 @@ USEFULL_LINKS_CONFIG = {
"url": {
"type": "url",
"required": True,
"validators": {}
"validators": {
"max_length": 300
}
},
"description": {
"type": "text",
"required": True,
"validators": {
"max_length": 500
"max_length": 50
}
}
}
......
......@@ -23,6 +23,9 @@ def validate_url(config, string):
validator_content = validators[validator]
if validator == 'extension':
validate_extension(validator_content, string)
elif validator == 'max_length':
if len(string) > validator_content:
raise ValidationError('Your url is too long !')
else:
raise Exception("Dev, you have implement something here...")
......
from django.conf import settings
from django.http import HttpResponse
import json
from backend.permissions.obj_moderation_permission import OBJ_MODERATION_PERMISSIONS
def app_moderation_status(request):
return HttpResponse(json.dumps({
'activated': settings.MODERATION_ACTIVATED,
'moderator_level': OBJ_MODERATION_PERMISSIONS["moderator"]
}))
......@@ -4,7 +4,7 @@ from os import makedirs
from os.path import join, dirname, realpath, exists
from django import template
import re
import yaml
from general.api import get_api_config
############
# Need to do this first so that Django template engine is working
......@@ -37,10 +37,8 @@ templates = [
'combinedReducers'
]
api_config = get_api_config()
API_BASE = "/api/"
with open(join(current_dir, '../../general/api/api_config.yml'), 'r') as f:
data = f.read()
api_config = yaml.load(data)
contexts = []
for api in api_config:
......@@ -53,6 +51,12 @@ for api in api_config:
"api_end_point": API_BASE + api["api_end_point"] + '/',
})
# API outside rest framework here
contexts.append({
"name": 'serverModerationStatus',
"api_end_point": API_BASE + 'serverModerationStatus' + '/',
})
def convert(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
......
......@@ -22,6 +22,7 @@ export const {{obj.NAME}}_EL_FETCH_DATA_SUCCESS = '{{obj.NAME}}_EL_FETCH_DATA_SU
{% if not obj.read_only %}
export const {{obj.NAME}}_EL_IS_SAVING = '{{obj.NAME}}_EL_IS_SAVING';
export const {{obj.NAME}}_EL_SHARE_SAVED_TIME = '{{obj.NAME}}_EL_SHARE_SAVED_TIME';
export const {{obj.NAME}}_EL_SAVING_HAS_ERROR = '{{obj.NAME}}_EL_SAVING_HAS_ERROR';
{% endif %}
......
......@@ -4,7 +4,8 @@
// MODIFY THE FILE ABOVE IF YOUR NOT SATISFIED
// THIS WARNING DOESN'T APPLY TO .tpl FILES...
import Cookies from 'js-cookie';
import SmartActions from '../actions/SmartActions';
import {
{% for obj in data %}
......@@ -21,7 +22,7 @@ import {
{% if not obj.read_only %}
{{obj.NAME}}_EL_IS_SAVING,
{{obj.NAME}}_EL_SAVING_HAS_ERROR,
{{obj.NAME}}_EL_SAVING_DATA_SUCCESS,
{{obj.NAME}}_EL_SHARE_SAVED_TIME,
{% endif %}
{% endfor %}
......@@ -30,79 +31,7 @@ import {
//////////////////////////////////
// generic function definitions
function _FetchData(pk, api_end_point, _IsLoading, _FetchDataSuccess, _Invalidated, _HasError, pk_required=false) {
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');
fetch(api_end_point, {credentials: 'same-origin',headers: {'X-CSRFToken': token}})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((obj) => {
dispatch(_Invalidated(false));
dispatch(_FetchDataSuccess(obj));
dispatch(_IsLoading(false));
})
.catch((e) => {
dispatch(_HasError(true, e));
dispatch(_IsLoading(false));
});
};
}
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 + '/';
}
dispatch(_ElIsSaving(true));
let token = Cookies.get('csrftoken');
fetch(api_end_point+pk, {
method: method,
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': token
},
body: JSON.stringify(data)
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((_El) => {
dispatch(_ElInvalidated(false));
dispatch(_ElFetchDataSuccess(_El)); // we use the same here
dispatch(_ElIsSaving(false));
})
.catch((e) => {
dispatch(_ElHasError(true, e))
dispatch(_ElIsSaving(false));
});
};
}
const smartActions = new SmartActions();
/////////////////////////////////
{% for obj in data %}
......@@ -128,20 +57,25 @@ export function {{obj.name}}Invalidated(bool) {
};
}
export function {{obj.name}}FetchDataSuccess({{obj.name}}) {
{{obj.name}}Invalidated(false)
export function {{obj.name}}FetchDataSuccess({{obj.name}}, setInvalidateFalse=true) {
let time = null;
if (setInvalidateFalse){
{{obj.name}}Invalidated(false)
time = Date.now();
}
return {
type: {{obj.NAME}}_FETCH_DATA_SUCCESS,
{{obj.name}},
{{obj.name}}FetchedAt: Date.now()
{{obj.name}}FetchedAt: time
};
}
export function {{obj.name}}FetchData() {
return _FetchData(
"",
export function {{obj.name}}FetchData(pk="") {
return smartActions._FetchData(
pk,
"{{obj.api_end_point}}",
{{obj.name}}IsLoading,
{{obj.name}}FetchDataSuccess,
......@@ -174,19 +108,23 @@ export function {{obj.name}}ElIsLoading(bool) {
}
export function {{obj.name}}ElFetchDataSuccess({{obj.name}}El) {
{{obj.name}}ElInvalidated(false)
export function {{obj.name}}ElFetchDataSuccess({{obj.name}}El, setInvalidateFalse=true) {
let time = null;
if (setInvalidateFalse){
{{obj.name}}ElInvalidated(false);
time = Date.now();
}
return {
type: {{obj.NAME}}_EL_FETCH_DATA_SUCCESS,
{{obj.name}}El,
{{obj.name}}ElFetchedAt: Date.now()
{{obj.name}}ElFetchedAt: time
};
}
export function {{obj.name}}ElFetchData(pk) {
return _FetchData(
return smartActions._FetchData(
pk,
"{{obj.api_end_point}}",
{{obj.name}}ElIsLoading,
......@@ -207,17 +145,23 @@ export function {{obj.name}}ElIsSaving(bool) {
};
}
export function {{obj.name}}ElSavingHasError(bool, error=null) {
export function {{obj.name}}ElSavingHasError(b, error=null) {
return {
type: {{obj.NAME}}_EL_SAVING_HAS_ERROR,
hasError: bool,
hasError: b,
error
};
}
export function {{obj.name}}ElShareSavedTime() {
return {
type: {{obj.NAME}}_EL_SHARE_SAVED_TIME,
time: Date.now()
};
}
export function {{obj.name}}ElSaveData(data) {
return _ElSaveData(data, "{{obj.api_end_point}}", {{obj.name}}ElIsSaving, {{obj.name}}ElFetchDataSuccess, {{obj.name}}ElInvalidated, {{obj.name}}ElSavingHasError)
return smartActions._ElSaveData(data, "{{obj.api_end_point}}", {{obj.name}}ElIsSaving, {{obj.name}}ElFetchDataSuccess, {{obj.name}}ElShareSavedTime, {{obj.name}}ElInvalidated, {{obj.name}}ElSavingHasError)
}
{% endif %}
......
......@@ -20,6 +20,7 @@ import {
{% if not obj.read_only %}
{{obj.name}}ElIsSaving,
{{obj.name}}ElShareSavedTime,
{{obj.name}}ElSavingHasError,
{% endif %}
......@@ -42,7 +43,8 @@ export const {{obj.name}}ElReducers = combineReducers({
{% if not obj.read_only %}
isSaving: {{obj.name}}ElIsSaving,
savingHasError: {{obj.name}}ElSavingHasError
savedAt: {{obj.name}}ElShareSavedTime,
savingHasError: {{obj.name}}ElSavingHasError,
{% endif %}
})
......
......@@ -19,6 +19,7 @@ import {
{% if not obj.read_only %}
{{obj.NAME}}_EL_IS_SAVING,
{{obj.NAME}}_EL_SAVING_HAS_ERROR,
{{obj.NAME}}_EL_SHARE_SAVED_TIME,
{{obj.NAME}}_EL_SAVING_DATA_SUCCESS,
{% endif %}
......@@ -135,6 +136,16 @@ export function {{obj.name}}ElIsSaving(state = false, action) {
}
export function {{obj.name}}ElShareSavedTime(state = null, action) {
switch (action.type) {
case {{obj.NAME}}_EL_SHARE_SAVED_TIME:
return action.time;
default:
return state;
}
}
export function {{obj.name}}ElSavingHasError(state = {status: false, error: null}, action) {
switch (action.type) {
case {{obj.NAME}}_EL_SAVING_HAS_ERROR:
......
import Cookies from 'js-cookie';
export default class SmartActions {
fetching = [];
shouldFetchEndPoint(endPoint) {
const now = Date.now();
// clear the list of old fetchs
// The same request won't be sent if the same one was sent within the past 500 ms
// Redux wasn't updating quick enough to prevent side effects of fetching the same stuff multiple times.
this.fetching = this.fetching.filter(obj => now - obj.fetchSendAt < 500)
if (this.fetching.find(obj => obj.endPoint == endPoint)) {
return false;
} else {
this.fetching.push({ endPoint, fetchSendAt: now });
return true;
}
}
_FetchData(pk, api_end_point, _IsLoading, _FetchDataSuccess, _Invalidated, _HasError, pk_required = false) {
if (pk_required && (typeof pk == 'undefined')) {
throw "pk shouldn't be empty when requesting a specific element";
}
if (pk != "") {
api_end_point += pk + '/';
}
if (!this.shouldFetchEndPoint(api_end_point)) {
return () => { };
}
return (dispatch) => {
dispatch(_IsLoading(true));
let token = Cookies.get('csrftoken');
fetch(api_end_point, { credentials: 'same-origin', headers: { 'X-CSRFToken': token } })
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((obj) => {
dispatch(_Invalidated(false));
dispatch(_FetchDataSuccess(obj));
dispatch(_IsLoading(false));
})
.catch((e) => {
dispatch(_HasError(true, e));
dispatch(_IsLoading(false));
});
};
}
_ElSaveData(data, api_end_point, _ElIsSaving, _ElFetchDataSuccess, _ElShareSavedTime, _ElInvalidated, _ElHasError) {
return (dispatch) => {
let method = "POST";
let pk = "";
if ('id' in data) {
method = "PUT";
pk = data.id + '/';
}
let __apiAttr = '';
if ('__apiAttr' in data && data.__apiAttr != '') {
__apiAttr = data.__apiAttr + '/';
}
let errorStatusText = '';
dispatch(_ElIsSaving(true));
let token = Cookies.get('csrftoken');
let f = fetch(api_end_point + __apiAttr + pk, {
method: method,
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': token
},
body: JSON.stringify(data)
})
.then((response) => {
if (!response.ok) {
errorStatusText = response.statusText;
throw response;
}
return response.json();
})
.then((_El) => {
dispatch(_ElInvalidated(false));
dispatch(_ElFetchDataSuccess(_El)); // we use the same here
dispatch(_ElShareSavedTime());
dispatch(_ElIsSaving(false));
})
.catch((e) => {
if (typeof e.json == 'function') {
return e.json()
} else {
return new Promise(function (resolve, reject) { resolve(e) });
}
})
.then((errorContent) => {
if (typeof errorContent != 'undefined') {
dispatch(_ElHasError(true, { message: errorStatusText, content: errorContent }));
dispatch(_ElIsSaving(false));
}
})
};
}
}
\ No newline at end of file
......@@ -3,7 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import withStyles from '@material-ui/core/styles/withStyles';
import CssBaseline from '@material-ui/core/CssBaseline';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
......@@ -16,7 +16,7 @@ import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
import SchoolIcon from '@material-ui/icons/School';
import { mainListItems, secondaryListItems, thirdListItems } from './template/listItems';
import { connect } from "react-redux";
import {connect} from "react-redux";
import MyComponent from './MyComponent'
// import route Components here
......
......@@ -3,85 +3,77 @@ import Loading from './other/Loading';
class MyComponent extends Component {
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 === Object(prop) && 'fetched' in prop) {
out[prop_key] = prop.fetched.data;
// __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;
ignoreInvalidation = false;
constructor(props) {
super(props);
// a little bit of optimization for later
this.fetchAbleProps = Array();
if (typeof props != 'undefined') {
const { fetchData } = props;
for (let propKey in fetchData) {
this.fetchAbleProps.push(propKey);
}
}
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;
// React default functions override
componentDidMount() {
this.loadPropsIfNeeded();
this.myComponentDidMount();
this.forceUpdate(); // bug otherwies
}
myComponentDidMount() { };
shouldComponentUpdate(nextProps, nextState) {
// Below is buggy with redux connect
// if (this.nextProps && typeof this.nextProps.visible != 'undefined' && !this.nextProps.visible) {
// // don't rerender components that won't be visible.
// return false;
// }
// if (this.props.visible === false && !this.nextProps) {
// // don't rerender components if it is still not visible
// return false;
// }
// // if (this.nextProps && typeof this.nextProps.visible != 'undefined' && this.nextProps.visible) {
// // // render if nextprops should be visible
// // return true;
// // }
return this.myShouldComponentUpdate(nextProps, nextState);
}
myShouldComponentUpdate(nextProps, nextState) { return true; }
componentDidUpdate(prevProps, prevState, snapshot) {
// TODO ajouter expire date
this.loadPropsIfNeeded();