Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
T
third-party
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
2
Merge Requests
2
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
cooperatic-foodcoops
third-party
Commits
32757349
Commit
32757349
authored
Mar 23, 2022
by
Etienne Freiss
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '2420-regular-shift-admin' into 'dev_cooperatic'
2420 regular shift admin See merge request
!148
parents
e0e9dc85
3795a517
Pipeline
#2006
passed with stage
in 1 minute 28 seconds
Changes
12
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
476 additions
and
6 deletions
+476
-6
admin.py
members/admin.py
+47
-0
models.py
members/models.py
+68
-4
manage_regular_shifts.css
members/static/css/admin/manage_regular_shifts.css
+89
-0
manage_shift_registrations.css
members/static/css/admin/manage_shift_registrations.css
+1
-1
bdm_index.js
members/static/js/admin/bdm_index.js
+2
-0
manage_makeups.js
members/static/js/admin/manage_makeups.js
+1
-1
manage_regular_shifts.js
members/static/js/admin/manage_regular_shifts.js
+184
-0
urls.py
members/urls.py
+2
-0
index.html
templates/members/admin/index.html
+4
-0
manage_makeups.html
templates/members/admin/manage_makeups.html
+1
-0
manage_regular_shifts.html
templates/members/admin/manage_regular_shifts.html
+76
-0
manage_shift_registrations.html
templates/members/admin/manage_shift_registrations.html
+1
-0
No files found.
members/admin.py
View file @
32757349
...
...
@@ -326,6 +326,13 @@ def manage_shift_registrations(request):
'module'
:
'Membres'
}
return
HttpResponse
(
template
.
render
(
context
,
request
))
def
manage_regular_shifts
(
request
):
""" Administration des créneaux des membres """
template
=
loader
.
get_template
(
'members/admin/manage_regular_shifts.html'
)
context
=
{
'title'
:
'BDM - Créneaux'
,
'module'
:
'Membres'
}
return
HttpResponse
(
template
.
render
(
context
,
request
))
def
get_makeups_members
(
request
):
""" Récupération des membres qui doivent faire des rattrapages """
res
=
CagetteMembers
.
get_makeups_members
()
...
...
@@ -408,4 +415,43 @@ def delete_shift_registration(request):
else
:
res
[
"message"
]
=
"Unauthorized"
response
=
JsonResponse
(
res
,
status
=
403
)
return
response
def
delete_shift_template_registration
(
request
):
""" From BDM admin, delete a member shift template registration """
res
=
{}
is_connected_user
=
CagetteUser
.
are_credentials_ok
(
request
)
if
is_connected_user
is
True
:
try
:
data
=
json
.
loads
(
request
.
body
.
decode
())
partner_id
=
int
(
data
[
"partner_id"
])
shift_template_id
=
int
(
data
[
"shift_template_id"
])
makeups_to_do
=
int
(
data
[
"makeups_to_do"
])
permanent_unsuscribe
=
data
[
"permanent_unsuscribe"
]
cm
=
CagetteMember
(
partner_id
)
# Get partner nb of future makeup shifts
partner_makeups
=
cm
.
get_member_selected_makeups
()
target_makeup
=
makeups_to_do
+
len
(
partner_makeups
)
if
target_makeup
>
2
:
target_makeup
=
2
# Update partner makeups to do
res
[
"update_makeups"
]
=
cm
.
update_member_makeups
({
'target_makeups_nb'
:
target_makeup
})
# Delete all shift registrations & shift template registration
res
[
"unsuscribe_member"
]
=
cm
.
unsuscribe_member
()
if
permanent_unsuscribe
is
True
:
res
[
"set_done"
]
=
cm
.
set_cooperative_state
(
"gone"
)
except
Exception
as
e
:
res
[
"error"
]
=
str
(
e
)
response
=
JsonResponse
(
res
,
safe
=
False
)
else
:
res
[
"message"
]
=
"Unauthorized"
response
=
JsonResponse
(
res
,
status
=
403
)
return
response
\ No newline at end of file
members/models.py
View file @
32757349
...
...
@@ -27,8 +27,7 @@ class CagetteMember(models.Model):
'display_ftop_points'
,
'display_std_points'
,
'is_exempted'
,
'cooperative_state'
,
'date_alert_stop'
]
m_shoft_default_fields
=
[
'name'
,
'barcode_base'
,
'total_partner_owned_share'
,
'amount_subscription'
]
m_short_default_fields
=
[
'name'
,
'barcode_base'
]
def
__init__
(
self
,
id
):
"""Init with odoo id."""
...
...
@@ -97,7 +96,6 @@ class CagetteMember(models.Model):
# # # BDM
def
save_partner_info
(
self
,
partner_id
,
fieldsDatas
):
print
(
fieldsDatas
)
return
self
.
o_api
.
update
(
'res.partner'
,
partner_id
,
fieldsDatas
)
...
...
@@ -822,8 +820,27 @@ class CagetteMember(models.Model):
members
.
append
(
m
)
return
CagetteMember
.
add_next_shifts_to_members
(
members
)
elif
search_type
==
"shift_template_data"
:
fields
=
CagetteMember
.
m_short_default_fields
fields
=
fields
+
[
'id'
,
'makeups_to_do'
,
'cooperative_state'
]
res
=
api
.
search_read
(
'res.partner'
,
cond
,
fields
)
if
res
:
for
partner
in
res
:
c
=
[[
'partner_id'
,
'='
,
int
(
partner
[
'id'
])],
[
'state'
,
'in'
,
(
'draft'
,
'open'
)]]
f
=
[
'shift_template_id'
]
shift_template_reg
=
api
.
search_read
(
'shift.template.registration'
,
c
,
f
)
if
shift_template_reg
:
partner
[
'shift_template_id'
]
=
shift_template_reg
[
0
][
'shift_template_id'
]
else
:
partner
[
'shift_template_id'
]
=
None
return
res
else
:
fields
=
CagetteMember
.
m_shoft_default_fields
# TODO differentiate short & subscription_data searches
fields
=
CagetteMember
.
m_short_default_fields
fields
=
fields
+
[
'total_partner_owned_share'
,
'amount_subscription'
]
res
=
api
.
search_read
(
'res.partner'
,
cond
,
fields
)
return
res
...
...
@@ -911,6 +928,53 @@ class CagetteMember(models.Model):
return
res
def
get_member_selected_makeups
(
self
):
res
=
{}
c
=
[[
"partner_id"
,
"="
,
self
.
id
],
[
"is_makeup"
,
"="
,
True
],
[
"state"
,
"="
,
"open"
]]
f
=
[
'id'
]
res
=
self
.
o_api
.
search_read
(
"shift.registration"
,
c
,
f
)
return
res
def
unsuscribe_member
(
self
):
res
=
{}
now
=
datetime
.
datetime
.
now
()
.
isoformat
()
# Get and then delete shift template registration
c
=
[[
'partner_id'
,
'='
,
self
.
id
]]
f
=
[
'id'
]
res_ids
=
self
.
o_api
.
search_read
(
"shift.template.registration"
,
c
,
f
)
ids
=
[
d
[
'id'
]
for
d
in
res_ids
]
if
ids
:
res
[
"delete_shift_template_reg"
]
=
self
.
o_api
.
execute
(
'shift.template.registration'
,
'unlink'
,
ids
)
# Get and then delete shift registrations
c
=
[[
'partner_id'
,
'='
,
self
.
id
],
[
'date_begin'
,
'>'
,
now
]]
f
=
[
'id'
]
res_ids
=
self
.
o_api
.
search_read
(
"shift.registration"
,
c
,
f
)
ids
=
[
d
[
'id'
]
for
d
in
res_ids
]
if
ids
:
res
[
"delete_shifts_reg"
]
=
self
.
o_api
.
execute
(
'shift.registration'
,
'unlink'
,
ids
)
# Close extensions
c
=
[[
'partner_id'
,
'='
,
self
.
id
],
[
'date_start'
,
'<='
,
now
],
[
'date_stop'
,
'>='
,
now
]]
f
=
[
'id'
]
res_ids
=
self
.
o_api
.
search_read
(
"shift.extension"
,
c
,
f
)
ids
=
[
d
[
'id'
]
for
d
in
res_ids
]
if
ids
:
f
=
{
'date_stop'
:
now
}
res
[
"close_extensions"
]
=
self
.
o_api
.
update
(
'shift.extension'
,
ids
,
f
)
return
res
def
set_cooperative_state
(
self
,
state
):
f
=
{
'cooperative_state'
:
state
}
return
self
.
o_api
.
update
(
'res.partner'
,
[
self
.
id
],
f
)
def
update_extra_shift_done
(
self
,
value
):
api
=
OdooAPI
()
res
=
{}
...
...
members/static/css/admin/manage_regular_shifts.css
0 → 100644
View file @
32757349
.header
{
margin
:
1rem
0
;
}
.login_area
{
position
:
absolute
;
display
:
block
;
top
:
5px
;
right
:
5px
;
}
#back_to_admin_index
{
position
:
absolute
;
top
:
5px
;
left
:
5px
;
}
/* Search members area */
#search_member_area
{
margin-top
:
30px
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
}
#search_member_form_area
{
display
:
flex
;
align-items
:
center
;
}
#search_member_form
{
margin-left
:
10px
;
}
.search_member_results_area
{
margin-top
:
15px
;
display
:
flex
;
align-items
:
center
;
}
.search_results_text
{
min-width
:
150px
;
}
.search_member_results
{
display
:
flex
;
flex-wrap
:
wrap
;
}
.btn_possible_member
{
margin
:
0.5rem
1rem
;
}
/* Member info area */
#partner_data_area
{
width
:
80%
;
display
:
none
;
flex-direction
:
column
;
align-items
:
center
;
border-radius
:
30px
;
margin
:
30px
auto
;
padding
:
15px
;
box-shadow
:
2px
2px
3px
rgba
(
10
,
10
,
10
,
.1
),
0
0
0
1px
rgba
(
10
,
10
,
10
,
.1
);
}
.member_name_container
{
margin
:
1rem
0
2rem
0
;
}
.member_name_icon
{
color
:
#00a573
;
margin-right
:
5px
;
}
.member_info
{
font-weight
:
bold
;
}
/* Actions */
#remove_shift_template_button
{
display
:
none
;
margin
:
15px
;
}
.checkbox_area
{
margin
:
10px
0
;
}
#permanent_unsuscribe
{
margin-right
:
5px
;
}
\ No newline at end of file
members/static/css/admin/manage_shift_registrations.css
View file @
32757349
...
...
@@ -38,11 +38,11 @@
padding-top
:
0.755em
;
}
/* Search members area */
.makeup_row
{
background-color
:
#ffc854
!important
;
}
/* Search membres area */
#search_member_area
{
margin-top
:
30px
;
display
:
flex
;
...
...
members/static/js/admin/bdm_index.js
View file @
32757349
...
...
@@ -15,6 +15,8 @@ $(document).ready(function() {
window
.
location
.
assign
(
location
+
"/manage_shift_registrations"
);
}
else
if
(
this
.
id
==
'manage_leaves_button'
)
{
console
.
log
(
'coming soon...'
);
}
else
if
(
this
.
id
==
'manage_regular_shifts_button'
)
{
window
.
location
.
assign
(
location
+
"/manage_regular_shifts"
);
}
});
}
else
{
...
...
members/static/js/admin/manage_makeups.js
View file @
32757349
...
...
@@ -371,7 +371,7 @@ $(document).ready(function() {
error
:
function
()
{
err
=
{
msg
:
"erreur serveur lors de la recherche de membres"
,
ctx
:
'
confirm_movement
.search_members'
ctx
:
'
members.admin.manage_makeups
.search_members'
};
report_JS_error
(
err
,
'stock'
);
...
...
members/static/js/admin/manage_regular_shifts.js
0 → 100644
View file @
32757349
var
selected_member
=
null
,
possible_cooperative_state
=
{
suspended
:
"Rattrapage"
,
exempted
:
"Exempté.e"
,
alert
:
"En alerte"
,
up_to_date
:
"À jour"
,
unsubscribed
:
"Désinscrit.e des créneaux"
,
delay
:
"En délai"
,
gone
:
"Parti.e"
};
/**
* Send request to remove partner from shift template
*/
function
remove_from_shift_template
()
{
let
permanent_unsuscribe
=
modal
.
find
(
"#permanent_unsuscribe"
).
prop
(
'checked'
);
openModal
();
let
data
=
{
partner_id
:
selected_member
.
id
,
shift_template_id
:
selected_member
.
shift_template_id
[
0
],
permanent_unsuscribe
:
permanent_unsuscribe
,
makeups_to_do
:
selected_member
.
makeups_to_do
,
};
$
.
ajax
({
type
:
'POST'
,
url
:
'/members/delete_shift_template_registration'
,
data
:
JSON
.
stringify
(
data
),
dataType
:
"json"
,
traditional
:
true
,
contentType
:
"application/json; charset=utf-8"
,
success
:
function
()
{
selected_member
.
shift_template_id
=
null
;
selected_member
.
cooperative_state
=
(
permanent_unsuscribe
===
true
)
?
"gone"
:
"unsubscribed"
;
display_member_info
();
closeModal
();
},
error
:
function
()
{
err
=
{
msg
:
"erreur serveur lors de la suppression du membre du créneau"
,
ctx
:
'members.admin.manage_regular_shifts.remove_from_shift_template'
};
report_JS_error
(
err
,
'members.admin'
);
closeModal
();
$
.
notify
(
"Une erreur est survenue lors du processus de suppression du membre du créneau."
,
{
globalPosition
:
"top right"
,
className
:
"error"
});
}
});
}
/**
* When a member is selected, display the selected member relevant info
*/
function
display_member_info
()
{
$
(
'.member_name'
).
text
(
`
${
selected_member
.
barcode_base
}
-
${
selected_member
.
name
}
`
);
$
(
'.member_status'
).
text
(
possible_cooperative_state
[
selected_member
.
cooperative_state
]);
$
(
'.member_makeups'
).
text
(
selected_member
.
makeups_to_do
);
if
(
selected_member
.
shift_template_id
===
undefined
||
selected_member
.
shift_template_id
===
null
)
{
$
(
'.member_shift'
).
text
(
""
);
$
(
"#remove_shift_template_button"
).
hide
();
$
(
"#remove_shift_template_button"
).
off
();
}
else
{
$
(
'.member_shift'
).
text
(
selected_member
.
shift_template_id
[
1
]);
$
(
"#remove_shift_template_button"
).
show
();
$
(
"#remove_shift_template_button"
).
off
();
$
(
"#remove_shift_template_button"
).
on
(
"click"
,
()
=>
{
let
modal_template
=
$
(
"#modal_remove_shift_template"
);
modal_template
.
find
(
".shift_template_name"
).
text
(
selected_member
.
shift_template_id
[
1
]);
openModal
(
modal_template
.
html
(),
remove_from_shift_template
,
"Valider"
,
false
);
});
}
$
(
'#search_member_input'
).
val
();
$
(
'#partner_data_area'
).
css
(
'display'
,
'flex'
);
}
/**
* Display the members from the search result
*/
function
display_possible_members
()
{
$
(
'.search_member_results_area'
).
show
();
$
(
'.search_member_results'
).
empty
();
$
(
'.btn_possible_member'
).
off
();
let
no_result
=
true
;
if
(
members_search_results
.
length
>
0
)
{
for
(
member
of
members_search_results
)
{
$
(
".search_results_text"
).
show
();
no_result
=
false
;
// Display results (possible members) as buttons
var
member_button
=
'<button class="btn--success btn_possible_member" member_id="'
+
member
.
id
+
'">'
+
member
.
barcode_base
+
' - '
+
member
.
name
+
'</button>'
;
$
(
'.search_member_results'
).
append
(
member_button
);
}
// Set action on member button click
$
(
'.btn_possible_member'
).
on
(
'click'
,
function
()
{
for
(
member
of
members_search_results
)
{
if
(
member
.
id
==
$
(
this
).
attr
(
'member_id'
))
{
selected_member
=
member
;
display_member_info
();
$
(
'.search_member_results'
).
empty
();
$
(
'.search_member_results_area'
).
hide
();
$
(
'#search_member_input'
).
val
(
''
);
break
;
}
}
});
}
if
(
no_result
===
true
)
{
$
(
".search_results_text"
).
hide
();
$
(
'.search_member_results'
).
html
(
`<p>
<i>Aucun résultat ! Vérifiez votre recherche...</i>
</p>`
);
}
}
$
(
document
).
ready
(
function
()
{
if
(
coop_is_connected
())
{
$
.
ajaxSetup
({
headers
:
{
"X-CSRFToken"
:
getCookie
(
'csrftoken'
)
}
});
$
(
".page_content"
).
show
();
// Set action to search for the member
$
(
'#search_member_form'
).
submit
(
function
()
{
let
search_str
=
$
(
'#search_member_input'
).
val
();
$
.
ajax
({
url
:
`/members/search/
${
search_str
}
?search_type=shift_template_data`
,
dataType
:
'json'
,
success
:
function
(
data
)
{
$
(
'#partner_data_area'
).
hide
();
if
(
data
.
res
.
length
===
1
)
{
selected_member
=
data
.
res
[
0
];
display_member_info
();
}
else
{
members_search_results
=
data
.
res
;
display_possible_members
();
}
},
error
:
function
()
{
err
=
{
msg
:
"erreur serveur lors de la recherche de membres"
,
ctx
:
'members.admin.manage_regular_shifts.search_members'
};
report_JS_error
(
err
,
'members.admin'
);
$
.
notify
(
"Erreur lors de la recherche de membre, il faut ré-essayer plus tard..."
,
{
globalPosition
:
"top right"
,
className
:
"error"
});
}
});
});
}
else
{
$
(
".page_content"
).
hide
();
}
$
(
'#back_to_admin_index'
).
on
(
'click'
,
function
()
{
let
base_location
=
window
.
location
.
href
.
split
(
"manage_regular_shifts"
)[
0
].
slice
(
0
,
-
1
);
window
.
location
.
assign
(
base_location
);
});
});
members/urls.py
View file @
32757349
...
...
@@ -58,7 +58,9 @@ urlpatterns = [
url
(
r'^admin/?$'
,
admin
.
admin
),
url
(
r'^admin/manage_makeups$'
,
admin
.
manage_makeups
),
url
(
r'^admin/manage_shift_registrations$'
,
admin
.
manage_shift_registrations
),
url
(
r'^admin/manage_regular_shifts$'
,
admin
.
manage_regular_shifts
),
url
(
r'^get_makeups_members$'
,
admin
.
get_makeups_members
),
url
(
r'^update_members_makeups$'
,
admin
.
update_members_makeups
),
url
(
r'^delete_shift_registration$'
,
admin
.
delete_shift_registration
),
url
(
r'^delete_shift_template_registration$'
,
admin
.
delete_shift_template_registration
),
]
templates/members/admin/index.html
View file @
32757349
...
...
@@ -34,6 +34,10 @@
<span
class=
"management_type_button_icons"
><i
class=
"fas fa-wrench"
></i></span>
{#
<span
class=
"management_type_button_icons"
><i
class=
"fas fa-arrow-right"
></i></span>
#}
</button><br>
<button
type=
"button"
class=
"btn--primary management_type_button"
id=
"manage_regular_shifts_button"
>
Gestion des créneaux
<span
class=
"management_type_button_icons"
><i
class=
"fas fa-arrow-right"
></i></span>
</button><br>
<button
type=
"button"
class=
"btn--primary management_type_button"
id=
"manage_leaves_button"
disabled
>
Gestion des congés
<span
class=
"management_type_button_icons"
><i
class=
"fas fa-wrench"
></i></span>
...
...
templates/members/admin/manage_makeups.html
View file @
32757349
...
...
@@ -11,6 +11,7 @@
{% block additionnal_scripts %}
<script
type=
"text/javascript"
src=
"{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"
></script>
<script
type=
"text/javascript"
src=
"{% static 'js/datatables/datatables.min.js' %}"
></script>
<script
type=
"text/javascript"
src=
"{% static 'js/notify.min.js' %}?v="
></script>
{% endblock %}
{% block content %}
...
...
templates/members/admin/manage_regular_shifts.html
0 → 100644
View file @
32757349
{% extends "base.html" %}
{% load static %}
{% block additionnal_css %}
<link
rel=
"stylesheet"
href=
"{% static 'css/datatables/datatables.min.css' %}"
>
<link
rel=
"stylesheet"
href=
"{% static 'css/admin/manage_regular_shifts.css' %}"
>
<link
rel=
"stylesheet"
href=
"{% static 'jquery-ui-1.12.1/jquery-ui.min.css' %}"
>
{% endblock %}
{% block additionnal_scripts %}
<script
type=
"text/javascript"
src=
"{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"
></script>
<script
type=
"text/javascript"
src=
"{% static 'js/datatables/datatables.min.js' %}"
></script>
<script
type=
"text/javascript"
src=
"{% static 'js/notify.min.js' %}?v="
></script>
{% endblock %}
{% block content %}
<div
class=
"page_body"
>
<div
id=
"back_to_admin_index"
>
<button
type=
"button"
class=
"btn--danger"
><i
class=
"fas fa-arrow-left"
></i>
Retour
</button>
</div>
<div
class=
"login_area"
>
{% include "common/conn_admin.html" %}
</div>
<div
class=
"header txtcenter"
>
<h1>
Gestion des Créneaux
</h1>
</div>
<div
class=
"page_content"
>
<div
id=
"search_member_area"
>
<div
id=
"search_member_form_area"
>
<h4>
Rechercher un.e membre
</h4>
<form
id=
"search_member_form"
action=
"javascript:;"
method=
"post"
>
<input
type=
"text"
id=
"search_member_input"
value=
""
placeholder=
"Nom ou numéro du coop..."
required
>
<button
type=
"submit"
class=
"btn--primary"
id=
"search_member_button"
>
Recherche
</button>
</form>
</div>
<div
class=
"search_member_results_area"
style=
"display:none;"
>
<div
class=
"search_results_text"
>
<p><i>
Choisissez parmi les membres trouvés :
</i></p>
</div>
<div
class=
"search_member_results"
></div>
</div>
</div>
<div
id=
"partner_data_area"
>
<h4
class=
"member_name_container"
>
<i
class=
"fas fa-user member_name_icon"
></i>
<span
class=
"member_info member_name"
></span>
</h4>
<p
class=
"shift_name_container"
>
Créneau :
<span
class=
"member_info member_shift"
></span></p>
<p
class=
"status_container"
>
Statut :
<span
class=
"member_info member_status"
></span></p>
<p
class=
"makeups_container"
>
Nb rattrapage(s) :
<span
class=
"member_info member_makeups"
></span></p>
<div
id=
"actions_on_member"
>
<button
class=
"btn--primary"
id=
"remove_shift_template_button"
>
Désinscrire du créneau
</button>
</div>
</div>
</div>
<div
id=
"templates"
style=
"display:none;"
>
<div
id=
"modal_remove_shift_template"
>
<p>
Voulez vraiment désinscrire ce membre du créneau
<span
class=
"shift_template_name"
></span>
?
</p>
<div
class=
"checkbox_area"
>
<input
type=
"checkbox"
id=
"permanent_unsuscribe"
name=
"permanent_unsuscribe"
>
<label
for=
"permanent_unsuscribe"
>
Désinscription définitive
</label>
</div>
</div>
</div>
</div>
<script
src=
'{% static "js/all_common.js" %}?v='
></script>
<script
src=
'{% static "js/admin/manage_regular_shifts.js" %}?v='
></script>
{% endblock %}
templates/members/admin/manage_shift_registrations.html
View file @
32757349
...
...
@@ -11,6 +11,7 @@
{% block additionnal_scripts %}
<script
type=
"text/javascript"
src=
"{% static 'jquery-ui-1.12.1/jquery-ui.min.js' %}"
></script>
<script
type=
"text/javascript"
src=
"{% static 'js/datatables/datatables.min.js' %}"
></script>
<script
type=
"text/javascript"
src=
"{% static 'js/notify.min.js' %}?v="
></script>
{% endblock %}
{% block content %}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment