Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Rex Dri
Rex Dri
Commits
7255c33d
Commit
7255c33d
authored
Sep 11, 2018
by
Florent Chehab
Browse files
Render Generic Module partly connected
parent
aa616a2a
Changes
23
Hide whitespace changes
Inline
Side-by-side
backend/generate/templates/urls.tpl
View file @
7255c33d
...
...
@@ -8,6 +8,8 @@
from django.conf.urls import url, include
from django.conf import settings
from rest_framework import routers
from django.urls import path
from . import views
ALL_MODELS = []
ALL_VIEWSETS = []
...
...
@@ -51,6 +53,7 @@ router.register(
){% endif %}{% endfor %}
urlpatterns += [url(r'^api/', include(router.urls))]
urlpatterns.append(path('api/serverModerationStatus/', views.app_moderation_status))
for model in ALL_MODELS:
for key in model.model_config:
...
...
backend/models/user/userData.py
View file @
7255c33d
...
...
@@ -4,13 +4,14 @@ from backend.models.university import University
from
backend.fields
import
JSONField
from
backend.models.abstract.my_model
import
MyModel
,
MyModelSerializer
,
MyModelViewSet
from
django.contrib.auth.models
import
User
from
backend.utils
import
get_viewset_permissions
,
get_model_config
from
backend.utils
import
get_viewset_permissions
,
get_model_config
,
get_user_level
class
UserData
(
MyModel
):
model_config
=
get_model_config
(
"UserData"
)
owner
=
models
.
OneToOneField
(
User
,
on_delete
=
models
.
CASCADE
,
primary_key
=
True
)
owner
=
models
.
OneToOneField
(
User
,
on_delete
=
models
.
CASCADE
,
primary_key
=
True
)
contact_info
=
JSONField
(
default
=
dict
)
contact_info_is_public
=
models
.
BooleanField
(
default
=
False
)
config
=
JSONField
(
default
=
dict
)
...
...
@@ -21,6 +22,10 @@ class UserData(MyModel):
class
UserDataSerializer
(
MyModelSerializer
):
owner
=
serializers
.
CharField
(
read_only
=
True
)
owner_level
=
serializers
.
SerializerMethodField
()
def
get_owner_level
(
self
,
obj
):
return
get_user_level
(
obj
.
owner
)
def
my_pre_save
(
self
):
self
.
override_validated_data
({
'owner'
:
self
.
user
})
...
...
backend/permissions/__is_moderation_required.py
View file @
7255c33d
...
...
@@ -3,6 +3,15 @@ from django.conf import settings
from
.obj_moderation_permission
import
OBJ_MODERATION_PERMISSIONS
######################################
######################################
##
# IF YOU TOUCH THIS FILE, MODIFY IT'S
# JS EQUIVALENT (isModerationRequired)
##
######################################
######################################
def
is_moderation_required
(
model_moderation_level
,
obj_in_db
,
user
,
user_level
=
None
):
if
user_level
is
None
:
user_level
=
get_user_level
(
user
)
...
...
backend/utils/__get_model_config.py
View file @
7255c33d
...
...
@@ -8,7 +8,8 @@ def get_model_config(model):
if
obj
[
'model'
]
==
model
:
return
{
"moderation_level"
:
obj
[
"moderation_level"
],
"model"
:
model
"model"
:
model
,
"read_only"
:
obj
[
"read_only"
]
}
raise
Exception
(
"Model not found in API configuraiton, cannot process !"
)
backend/views.py
0 → 100644
View file @
7255c33d
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"
]
}))
frontend/generate/generate_frontend_files.py
View file @
7255c33d
...
...
@@ -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
)
...
...
frontend/src/components/MyComponent.js
View file @
7255c33d
import
React
,
{
Component
}
from
'
react
'
;
import
Loading
from
'
./other/Loading
'
;
import
_
from
'
underscore
'
;
class
MyComponent
extends
Component
{
customErrorHandlers
=
{}
idToUse
=
null
;
...
...
@@ -14,9 +14,6 @@ class MyComponent extends Component {
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
;
}
...
...
@@ -37,9 +34,6 @@ class MyComponent extends Component {
const
{
props
}
=
this
;
for
(
let
el
in
props
)
{
let
prop
=
props
[
el
];
// if (typeof prop == 'boolean') {
// continue;
// }
if
(
prop
===
Object
(
prop
)
&&
val
in
prop
&&
prop
[
val
])
{
return
true
;
}
...
...
@@ -51,9 +45,6 @@ class MyComponent extends Component {
const
{
props
}
=
this
;
for
(
let
prop_key
in
props
)
{
let
prop
=
props
[
prop_key
];
if
(
typeof
prop
==
'
boolean
'
)
{
continue
;
}
if
(
prop
===
Object
(
prop
)
&&
'
fetchHasError
'
in
prop
)
{
if
(
prop
.
fetchHasError
.
status
)
{
if
(
prop_key
in
this
.
customErrorHandlers
)
{
...
...
@@ -80,36 +71,35 @@ class MyComponent extends Component {
return
this
.
checkProps
(
'
isSaving
'
);
}
loadPropsIfNeeded
()
{
loadPropsIfNeeded
(
dontFetch
=
false
)
{
let
propsLoaded
=
true
;
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
===
Object
(
prop
)
&&
'
fetched
'
in
prop
)
{
if
((
!
prop
.
fetched
.
fetchedAt
)
||
prop
.
invalidated
)
{
if
(
!
prop
.
isLoading
)
{
if
(
this
.
idToUse
)
{
props
.
fetchData
[
prop_key
](
this
.
props
[
this
.
idToUse
]);
}
else
{
props
.
fetchData
[
prop_key
]();
propsLoaded
=
false
;
if
(
!
dontFetch
)
{
if
(
!
prop
.
isLoading
)
{
if
(
this
.
idToUse
)
{
props
.
fetchData
[
prop_key
](
this
.
props
[
this
.
idToUse
]);
}
else
{
props
.
fetchData
[
prop_key
]();
}
}
}
}
}
}
return
propsLoaded
;
}
allFetchedDataReady
(){
if
(
this
.
checkPropsHasError
()
||
this
.
checkPropsIsLoading
()
||
this
.
checkPropsInvalidated
()
||
this
.
checkPropsIsSaving
())
{
return
false
;
}
else
{
return
true
;
}
allFetchedDataAreReady
()
{
return
this
.
loadPropsIfNeeded
(
true
);
}
componentDidMount
()
{
...
...
@@ -130,7 +120,7 @@ class MyComponent extends Component {
return
<
p
>
Sorry
!
There
was
an
error
loading
the
items
<
/p>
;
}
if
(
!
this
.
allFetchedDataReady
())
{
if
(
!
this
.
allFetchedData
Are
Ready
())
{
return
<
Loading
/>
;
}
...
...
frontend/src/components/ThemeProvider.js
View file @
7255c33d
...
...
@@ -20,7 +20,7 @@ class ThemeProvider extends MyComponent {
state
=
{
theme
:
this
.
props
.
themeSavedInTheApp
.
theme
};
myComponentDidUpdate
()
{
if
(
!
this
.
allFetchedDataReady
())
{
if
(
!
this
.
allFetchedData
Are
Ready
())
{
return
;
}
...
...
frontend/src/components/settings/ColorTools.js
View file @
7255c33d
...
...
@@ -89,7 +89,7 @@ class ColorTool extends MyComponent {
}
myComponentDidUpdate
()
{
if
(
!
this
.
allFetchedDataReady
())
{
if
(
!
this
.
allFetchedData
Are
Ready
())
{
return
}
...
...
frontend/src/components/university/UniversityTemplate.js
View file @
7255c33d
...
...
@@ -88,11 +88,11 @@ class UniversityTemplate extends React.Component {
<
/Tabs
>
<
/AppBar
>
{
value
===
0
&&
<
TabContainer
>
<
GeneralInfoTab
univId
=
{
univId
}
/> </
TabContainer
>
}
{
value
===
1
&&
<
TabContainer
>
<
UniversityMoreTab
univId
=
{
univId
}
/> </
TabContainer
>
}
{
value
===
2
&&
<
TabContainer
>
<
PreviousDeparturesTab
univId
=
{
univId
}
/> </
TabContainer
>
}
{
value
===
3
&&
<
TabContainer
>
<
ScholarshipsTab
univId
=
{
univId
}
/> </
TabContainer
>
}
{
value
===
4
&&
<
TabContainer
>
<
CampusesCitiesTab
univId
=
{
univId
}
/> </
TabContainer
>
}
{
value
===
5
&&
<
TabContainer
>
<
MoreTab
univId
=
{
univId
}
/> </
TabContainer
>
}
{
/*
{value === 1 && <TabContainer> <UniversityMoreTab univId={univId} /> </TabContainer>}
*/
}
{
/*
{value === 2 && <TabContainer> <PreviousDeparturesTab univId={univId} /> </TabContainer>}
*/
}
{
/*
{value === 3 && <TabContainer> <ScholarshipsTab univId={univId} /> </TabContainer>}
*/
}
{
/*
{value === 4 && <TabContainer> <CampusesCitiesTab univId={univId} /> </TabContainer>}
*/
}
{
/*
{value === 5 && <TabContainer> <MoreTab univId={univId} /> </TabContainer>}
*/
}
<
/div
>
<
/div
>
...
...
frontend/src/components/university/modules/GenericModule.js
View file @
7255c33d
import
React
from
'
react
'
;
import
{
withStyles
}
from
'
@material-ui/core/styles
'
;
import
Tooltip
from
'
@material-ui/core/Tooltip
'
;
import
Button
from
'
@material-ui/core/Button
'
;
import
IconButton
from
'
@material-ui/core/IconButton
'
;
import
classNames
from
'
classnames
'
;
import
PropTypes
from
'
prop-types
'
;
import
{
withStyles
}
from
'
@material-ui/core/styles
'
;
import
{
compose
}
from
'
recompose
'
;
import
{
connect
}
from
"
react-redux
"
;
import
Grid
from
'
@material-ui/core/Grid
'
;
import
Typography
from
'
@material-ui/core/Typography
'
;
import
Paper
from
'
@material-ui/core/Paper
'
;
import
Chip
from
'
@material-ui/core/Chip
'
;
import
Avatar
from
'
@material-ui/core/Avatar
'
;
import
Divider
from
'
@material-ui/core/Divider
'
;
import
SettingsBackRestoreIcon
from
'
@material-ui/icons/SettingsBackupRestore
'
;
import
CreateIcon
from
'
@material-ui/icons/Create
'
;
import
VerifiedUserIcon
from
'
@material-ui/icons/VerifiedUser
'
;
import
LinkIcon
from
'
@material-ui/icons/Link
'
;
import
NotificationImportantIcon
from
'
@material-ui/icons/NotificationImportant
'
;
import
FullScreenDialog
from
'
../../shared/FullScreenDialog
'
;
import
MyBadge
from
'
../../shared/MyBadge
'
;
import
red
from
'
@material-ui/core/colors/red
'
;
import
orange
from
'
@material-ui/core/colors/orange
'
;
import
green
from
'
@material-ui/core/colors/green
'
;
import
MyComponent
from
'
../../MyComponent
'
;
import
{
serverModerationStatusFetchData
,
userDataElFetchData
}
from
'
../../../generated/actions
'
;
import
renderUsefulLinks
from
'
./genericModuleFunctions/renderUsefulLinks
'
;
import
renderFirstRow
from
'
./genericModuleFunctions/renderFirstRow
'
;
const
styles
=
theme
=>
({
root
:
{
flexGrow
:
1
,
...
...
@@ -63,7 +59,7 @@ const styles = theme => ({
chip
:
{
margin
:
theme
.
spacing
.
unit
,
},
titleContainer
:{
titleContainer
:
{
display
:
"
flex
"
,
alignItems
:
"
center
"
,
},
...
...
@@ -73,143 +69,9 @@ const styles = theme => ({
}
})
function
renderTitle
()
{
const
{
title
,
classes
,
importanceLevel
}
=
this
.
props
;
if
(
title
)
{
if
(
importanceLevel
)
{
let
c
=
classNames
(
classes
.
titleIcon
);
if
(
importanceLevel
==
"
IMPORTANT
"
){
c
=
classNames
(
classes
.
titleIcon
,
classes
.
red
)
}
else
if
(
importanceLevel
==
"
important
"
){
c
=
classNames
(
classes
.
titleIcon
,
classes
.
orange
)
}
return
(
<
div
className
=
{
classes
.
titleContainer
}
>
<
NotificationImportantIcon
className
=
{
c
}
/
>
<
Typography
variant
=
'
display1
'
>
{
title
}
<
/Typography
>
<
/div
>
)
}
else
{
return
(
<
Typography
variant
=
'
display1
'
>
{
title
}
<
/Typography
>
)
}
}
else
{
return
(
<
div
><
/div>
)
}
}
function
renderUpdateInfo
()
{
const
{
automaticData
}
=
this
.
props
;
if
(
automaticData
)
{
return
(
<
Typography
variant
=
'
caption
'
>
Dernière
mise
à
jour
le
08
/
09
/
2018
à
15
h20
<
/Typography
>
)
}
else
{
return
(
<
Typography
variant
=
'
caption
'
>
Mis
à
jour
par
<
em
>
chehabfl
<
/em> le 08/
09
/
2018
à
15
h20
<
/Typography
>
)
}
}
function
renderFirstRow
()
{
const
{
classes
,
title
,
automaticData
,
theme
}
=
this
.
props
;
let
classEdit
=
"
green
"
,
classModer
=
"
orange
"
,
classVersion
=
"
red
"
;
if
(
automaticData
)
{
classEdit
=
"
disabled
"
;
classModer
=
"
disabled
"
;
classVersion
=
"
disabled
"
;
}
return
(
<
Grid
container
spacing
=
{
8
}
>
<
Grid
item
xs
style
=
{{
paddingBottom
:
theme
.
spacing
.
unit
}}
>
{
renderTitle
.
bind
(
this
)()}
{
renderUpdateInfo
.
bind
(
this
)()}
<
/Grid
>
<
Grid
item
xs
=
{
4
}
style
=
{{
textAlign
:
'
right
'
}}
>
<
Tooltip
title
=
"
Informations sur la modération
"
placement
=
"
top
"
>
<
div
style
=
{{
display
:
'
inline-block
'
}}
>
{
/* Needed to fire events for the tooltip when below is disabled! when below is disabled!! */
}
<
MyBadge
classes
=
{{
badge
:
classes
.
badge
}}
badgeContent
=
{
null
}
color
=
"
secondary
"
>
<
IconButton
aria
-
label
=
"
Modération
"
disabled
=
{
automaticData
}
className
=
{
classes
.
button
}
>
<
VerifiedUserIcon
className
=
{
classes
[
classModer
]}
/
>
<
/IconButton
>
<
/MyBadge
>
<
/div
>
<
/Tooltip
>
<
Tooltip
title
=
"
Informations sur les possibilités d'éditions
"
placement
=
"
top
"
>
<
div
style
=
{{
display
:
'
inline-block
'
}}
>
{
/* Needed to fire events for the tooltip when below is disabled!! */
}
<
IconButton
aria
-
label
=
"
Éditer
"
className
=
{
classes
.
button
}
disabled
=
{
automaticData
}
onClick
=
{
this
.
handleClickOpenFullScreenDialog
}
>
<
CreateIcon
className
=
{
classes
[
classEdit
]}
/
>
<
/IconButton
>
<
/div
>
<
/Tooltip
>
<
Tooltip
title
=
"
Informations sur les versions disponibles
"
placement
=
"
top
"
>
<
div
style
=
{{
display
:
'
inline-block
'
}}
>
{
/* Needed to fire events for the tooltip when below is disabled!! */
}
<
MyBadge
classes
=
{{
badge
:
classes
.
badge
}}
badgeContent
=
{
40
}
color
=
"
secondary
"
>
<
IconButton
aria
-
label
=
"
Restorer
"
disabled
=
{
automaticData
}
className
=
{
classes
.
button
}
>
<
SettingsBackRestoreIcon
className
=
{
classes
[
classVersion
]}
/
>
<
/IconButton
>
<
/MyBadge
>
<
/div
>
<
/Tooltip
>
<
/Grid
>
<
/Grid
>
)
}
function
renderUsefulLinks
()
{
const
{
classes
,
usefulLinks
,
theme
}
=
this
.
props
;
const
nbItems
=
usefulLinks
.
length
;
if
(
nbItems
==
0
)
{
return
(
<
div
><
/div>
)
}
const
s
=
nbItems
>
1
?
"
s
"
:
""
;
return
(
<
div
>
<
Typography
variant
=
'
caption
'
style
=
{{
paddingTop
:
2
*
theme
.
spacing
.
unit
}}
>
Source
{
s
}
:
<
/Typography
>
<
div
className
=
{
classes
.
rootLinks
}
>
{
usefulLinks
.
map
((
el
,
index
)
=>
{
return
(
<
Chip
key
=
{
index
}
avatar
=
{
<
Avatar
>
<
LinkIcon
/>
<
/Avatar
>
}
label
=
{
el
.
description
}
className
=
{
classes
.
chip
}
color
=
"
secondary
"
onClick
=
{()
=>
open
(
el
.
url
,
'
_blank
'
)}
variant
=
"
outlined
"
/>
)
})
}
<
/div
>
<
/div
>
)
}
class
GenericModule
extends
React
.
Component
{
class
GenericModule
extends
MyComponent
{
state
=
{
fullScreenDialogOpen
:
false
,
};
...
...
@@ -222,14 +84,15 @@ class GenericModule extends React.Component {
this
.
setState
({
fullScreenDialogOpen
:
false
});
};
render
()
{
const
{
classes
,
title
}
=
this
.
props
;
myRender
()
{
console
.
log
(
renderFirstRow
)
const
{
classes
}
=
this
.
props
;
return
(
<
div
>
<
FullScreenDialog
open
=
{
this
.
state
.
fullScreenDialogOpen
}
handleClose
=
{
this
.
handleCloseFullScreenDialog
}
/
>
<
Paper
className
=
{
classes
.
root
}
square
=
{
true
}
>
{
renderFirstRow
.
bind
(
this
)()}
<
div
>
{
this
.
props
.
children
}
<
/div
>
...
...
@@ -243,9 +106,34 @@ class GenericModule extends React.Component {
GenericModule
.
defaultProps
=
{
title
:
null
,
importanceLevel
:
null
,
automaticData
:
false
,
usefulLinks
:
[]
usefulLinks
:
[],
};
GenericModule
.
propTypes
=
{
classes
:
PropTypes
.
object
.
isRequired
,
theme
:
PropTypes
.
object
.
isRequired
,
modelData
:
PropTypes
.
object
.
isRequired
};
export
default
withStyles
(
styles
,
{
withTheme
:
true
})(
GenericModule
);
\ No newline at end of file
const
mapStateToProps
=
(
state
)
=>
{
return
{
serverModerationStatus
:
state
.
app
.
serverModerationStatus
,
userDataEl
:
state
.
userDataEl
};
};
const
mapDispatchToProps
=
(
dispatch
)
=>
{
return
{
fetchData
:
{
userDataEl
:
()
=>
dispatch
(
userDataElFetchData
(
""
)),
serverModerationStatus
:
()
=>
dispatch
(
serverModerationStatusFetchData
()),
}
};
};
export
default
compose
(
withStyles
(
styles
,
{
withTheme
:
true
}),
connect
(
mapStateToProps
,
mapDispatchToProps
)
)(
GenericModule
);
frontend/src/components/university/modules/UniversitySemestersDates.js
View file @
7255c33d
...
...
@@ -12,22 +12,16 @@ import TableRow from '@material-ui/core/TableRow';
import
Markdown
from
'
../../shared/Markdown
'
;
import
Typography
from
'
@material-ui/core/Typography
'
;
import
CloudQueueIcon
from
'
@material-ui/icons/CloudQueue
'
;
import
LocalFloristIcon
from
'
@material-ui/icons/LocalFlorist
'
;
import
TextLink
from
'
../../other/TextLink
'
;
import
GenericModule
from
'
./GenericModule
'
;
import
Grid
from
'
@material-ui/core/Grid
'
;
import
Divider
from
'
@material-ui/core/Divider
'
;
import
MyComponent
from
'
../../MyComponent
'
;
import
{
universitiesSemestersDatesElFetchData
,
}
from
'
../../../generated/actions
'
;
const
styles
=
theme
=>
({
...
...
@@ -63,25 +57,19 @@ function convertDate(date) {
class
UniversitySemestersDates
extends
MyComponent
{
idToUse
=
"
univId
"
;
customErrorHandlers
=
{
universitiesSemestersDatesEl
:
(
e
)
=>
{
console
.
log
(
e
);
return
true
;
}
}
myRender
()
{
const
{
classes
}
=
this
.
props
;
console
.
log
(
"
props
"
,
this
.
props
)
const
semestersDates
=
this
.
getFetchedData
(
'
universitiesSemestersDatesEl
'
);
let
{
autumn_begin
,
autumn_end
,
spring_begin
,
spring_end
,
comment
}
=
semestersDates
;
console
.
log
(
"
semestersDates
"
,
semestersDates
);
autumn_begin
=
convertDate
(
autumn_begin
);
autumn_end
=
convertDate
(
autumn_end
);
spring_begin
=
convertDate
(
spring_begin
);
spring_end
=
convertDate
(
spring_end
);
return
(
<
GenericModule
title
=
{
"
Date des semestres
"
}
usefulLinks
=
{[{
description
:
"
Site de l'EPFL
"
,
url
:
"
https://epfl.ch
"
}]
}
>
<
GenericModule
title
=
{
"
Date des semestres
"
}
modelData
=
{
semestersDates
}
>
<
div
className
=
{
classes
.
root
}
>
<
Table
>
<
TableHead
>
...
...
frontend/src/components/university/modules/genericModuleFunctions/getEditTooltipAndClass.js
0 → 100644
View file @
7255c33d
import
isModerationRequired
from
'
../../../../utils/isModerationRequired
'
;
export
default
function
getEditTooltipAndClass
(
readOnly
,
modelData
,
userLevel
,
serverModerationStatus
,
moderatorLevel
)
{
if
(
readOnly
)
{
return
{
editTooltip
:
"
Ce contenu n'est pas concerné par l'édition.
"
,
editClass
:
"
disabled
"
}
}
if
(
isModerationRequired
(
serverModerationStatus
,
moderatorLevel
,
modelData
.
model_config
.
moderation_level
,
modelData
.
obj_moderation_level
,
userLevel
))
{
return
{
editTooltip
:
"
Vous pouvez éditer le contenu de ce module, mais votre contribution sera assujettie à la modération.
"
,
editClass
:
"
orange
"
}