Commit a7ecd0d6 by Félicie

Merge branch 'evolution_bdm' of gl.cooperatic.fr:cooperatic-foodcoops/third-party into ticket_1680

parents c31c58a7 934b8216
......@@ -134,5 +134,5 @@ BLOCK_SERVICE_EXCHANGE_24H_BEFORE = True
ORDERS_HELPER_METABASE_URL = "url_meta_base"
# New members space
USE_NEW_MEMBERS_SPACE = True
START_DATE_FOR_POINTS_HISTORY = "2018-01-01"
START_DATE_FOR_SHIFTS_HISTORY = "2018-01-01"
......@@ -109,14 +109,14 @@ def add_pts_to_everybody(request, pts, reason):
is_connected_user = CagetteUser.are_credentials_ok(request)
if is_connected_user is True:
try:
fields = ['in_ftop_team']
fields = ['shift_type']
cond = [['is_member', '=', True]]
all_members = CagetteMembers.get(cond, fields)
if all_members and len(all_members) > 0:
ftop_ids = []
standard_ids = []
for m in all_members:
if m['in_ftop_team'] is True:
if m['shift_type'] == 'ftop':
ftop_ids.append(m['id'])
else:
standard_ids.append(m['id'])
......
......@@ -21,7 +21,7 @@ FUNDRAISING_CAT_ID = {'A': 1, 'B': 2, 'C': 3}
class CagetteMember(models.Model):
"""Class to handle cagette Odoo member."""
m_default_fields = ['name', 'parent_name', 'sex', 'image_medium', 'active',
'barcode_base', 'barcode', 'in_ftop_team',
'barcode_base', 'barcode', 'shift_type',
'is_associated_people', 'is_member', 'shift_type',
'display_ftop_points', 'display_std_points',
'is_exempted', 'cooperative_state', 'date_alert_stop']
......@@ -118,19 +118,25 @@ class CagetteMember(models.Model):
fields = ['name', 'email', 'birthdate', 'create_date', 'cooperative_state', 'is_associated_people']
res = api.search_read('res.partner', cond, fields)
if (res and len(res) >= 1):
coop_id = None
for item in res:
coop = item
if item["birthdate"] is not False:
coop_birthdate = item['birthdate']
coop_state = item['cooperative_state']
if item["is_associated_people"] == True:
break
coop_id = item['id']
y, m, d = coop['birthdate'].split('-')
y, m, d = coop_birthdate.split('-')
password = password.replace('/', '')
if (password == d + m + y):
data['id'] = coop['id']
if coop_id is None:
coop_id = coop['id']
data['id'] = coop_id
auth_token_seed = fp + coop['create_date']
data['auth_token'] = hashlib.sha256(auth_token_seed.encode('utf-8')).hexdigest()
data['token'] = hashlib.sha256(coop['create_date'].encode('utf-8')).hexdigest()
data['coop_state'] = coop['cooperative_state']
data['coop_state'] = coop_state
if not ('auth_token' in data):
data['failure'] = True
......@@ -799,6 +805,20 @@ class CagetteMember(models.Model):
res['error'] = str(e)
return res
def search_associated_people(self):
""" Search for an associated partner """
res = {}
c = [["parent_id", "=", self.id]]
f = ["id", "name", "barcode_base"]
res = self.o_api.search_read('res.partner', c, f)
try:
return res[0]
except:
return None
class CagetteMembers(models.Model):
"""Class to manage operations on all members or part of them."""
......@@ -1117,24 +1137,26 @@ class CagetteServices(models.Model):
def get_services_at_time(time, tz_offset, with_members=True):
"""Retrieve present services with member linked."""
# import operator
min_before_shift_starts_delay = 20
min_after_shift_starts_delay = 20
default_acceptable_minutes_after_shift_begins = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_AFTER_SHIFT_BEGINS', 15)
minutes_before_shift_starts_delay = getattr(settings, 'ACCEPTABLE_ENTRANCE_MINUTES_BEFORE_SHIFT', 15)
minutes_after_shift_starts_delay = default_acceptable_minutes_after_shift_begins
late_mode = getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
max_duration = getattr(settings, 'MAX_DURATION', 180)
if late_mode is True:
min_before_shift_starts_delay = getattr(settings, 'ENTRANCE_VALIDATION_GRACE_DELAY', 60)
min_after_shift_starts_delay = 0
minutes_after_shift_starts_delay = getattr(settings, 'ENTRANCE_VALIDATION_GRACE_DELAY', 60)
api = OdooAPI()
now = dateutil.parser.parse(time) - datetime.timedelta(minutes=tz_offset)
start1 = now - datetime.timedelta(minutes=min_before_shift_starts_delay)
start2 = now + datetime.timedelta(minutes=min_after_shift_starts_delay)
cond = [['date_begin_tz', '>=', start1.isoformat()],
['date_begin_tz', '<=', start2.isoformat()]]
start1 = now + datetime.timedelta(minutes=minutes_before_shift_starts_delay)
start2 = now - datetime.timedelta(minutes=minutes_after_shift_starts_delay)
end = start1 + datetime.timedelta(minutes=max_duration)
cond = [['date_end_tz', '<=', end.isoformat()]]
cond.append('|')
cond.append(['date_begin_tz', '>=', start1.isoformat()])
cond.append(['date_begin_tz', '>=', start2.isoformat()])
fields = ['name', 'week_number', 'registration_ids',
'standard_registration_ids',
'shift_template_id', 'shift_ticket_ids',
'date_begin_tz', 'date_end_tz']
## return (start1.isoformat(), start2.isoformat())
services = api.search_read('shift.shift', cond, fields,order ="date_begin_tz ASC")
for s in services:
if (len(s['registration_ids']) > 0):
......@@ -1143,7 +1165,7 @@ class CagetteServices(models.Model):
now.replace(tzinfo=None)
-
dateutil.parser.parse(s['date_begin_tz']).replace(tzinfo=None)
).seconds / 60 > min_after_shift_starts_delay
).total_seconds() / 60 > default_acceptable_minutes_after_shift_begins
if with_members is True:
cond = [['id', 'in', s['registration_ids']], ['state', '!=', 'cancel']]
fields = ['partner_id', 'shift_type', 'state']
......@@ -1211,14 +1233,17 @@ class CagetteServices(models.Model):
return api.update('shift.registration', [int(reg_id)], f)
@staticmethod
def record_absences():
def record_absences(date):
"""Called by cron script."""
import dateutil.parser
now = datetime.datetime.now()
if len(date) > 0:
now = dateutil.parser.parse(date)
else:
now = datetime.datetime.now()
# now = dateutil.parser.parse('2020-09-15T15:00:00Z')
date_24h_before = now - datetime.timedelta(hours=24)
# let authorized people time to set presence for those who came in late
end_date = now - datetime.timedelta(hours=3)
end_date = now - datetime.timedelta(hours=2)
api = OdooAPI()
absence_status = 'excused'
res_c = api.search_read('ir.config_parameter',
......@@ -1250,7 +1275,10 @@ class CagetteServices(models.Model):
if int(_h) < 21:
ids.append(int(r['id']))
f = {'state': absence_status}
return {'update': api.update('shift.registration', ids, f), 'reg_shift': res}
update_shift_reg_result = {'update': api.update('shift.registration', ids, f), 'reg_shift': res}
if update_shift_reg_result['update'] is True:
update_shift_reg_result['process_status_res'] = api.execute('res.partner','run_process_target_status', [])
return update_shift_reg_result
@staticmethod
def close_ftop_service():
......
......@@ -43,7 +43,7 @@ h1 .member_name {font-weight: bold;}
{border:1px solid #000; border-radius: 5px; padding:5px; margin-bottom:15px;background:#FFF;}
.members_list {list-style: none;}
.members_list li {display:block;margin-bottom:5px;}
.members_list.late li {color: #de9b00;}
.members_list.late li {background-color: #de9b00; color: white}
.members_list li.btn--inverse {background: #449d44 !important; cursor:not-allowed; color: #FFF; }
#service_entry_success {font-size: x-large;}
......
......@@ -394,7 +394,7 @@ function fill_service_entry_sucess(member) {
var points = member.display_std_points;
if (member.in_ftop_team == true) {
if (member.shift_type == 'ftop') {
points = member.display_ftop_points;
}
pages.service_entry_success.find('span.points').text(points);
......@@ -410,7 +410,7 @@ function fill_service_entry_sucess(member) {
var service_verb = 'est prévu';
if (member.next_shift) {
if (member.in_ftop_team == true
if (member.shift_type == 'ftop'
&& member.next_shift.shift_type == "ftop") {
var start_elts = member.next_shift.start.split(' à ');
......@@ -468,7 +468,7 @@ function fill_rattrapage_2() {
var msg = "Bienvenue pour ton rattrapage !";
var shift_ticket_id = selected_service.shift_ticket_ids[0];
if (current_displayed_member.in_ftop_team == true) {
if (current_displayed_member.shift_type == 'ftop') {
msg ="Bienvenue dans ce service !";
if (selected_service.shift_ticket_ids[1])
shift_ticket_id = selected_service.shift_ticket_ids[1];
......
......@@ -40,7 +40,7 @@ urlpatterns = [
url(r'^save_photo/([0-9]+)$', views.save_photo, name='save_photo'),
url(r'^services_at_time/([0-9TZ\-\: \.]+)/([0-9\-]+)$', views.services_at_time),
url(r'^service_presence/$', views.record_service_presence),
url(r'^record_absences$', views.record_absences),
url(r'^record_absences/?([0-9\-\ \:]*)$', views.record_absences),
url(r'^close_ftop_service$', views.close_ftop_service),
url(r'^get_credentials$', views.get_credentials),
url(r'^remove_data_from_couchdb$', views.remove_data_from_CouchDB),
......
......@@ -29,7 +29,8 @@ def index(request):
'ENTRANCE_EASY_SHIFT_VALIDATE_MSG': getattr(settings, 'ENTRANCE_EASY_SHIFT_VALIDATE_MSG',
'Je valide mon service "Comité"'),
'CONFIRME_PRESENT_BTN' : getattr(settings, 'CONFIRME_PRESENT_BTN', 'Présent.e'),
'LATE_MODE': getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False)
'LATE_MODE': getattr(settings, 'ENTRANCE_WITH_LATE_MODE', False),
'ENTRANCE_VALIDATE_PRESENCE_MESSAGE' : getattr(settings, 'ENTRANCE_VALIDATE_PRESENCE_MESSAGE', '')
}
for_shoping_msg = getattr(settings, 'ENTRANCE_COME_FOR_SHOPING_MSG', '')
......@@ -281,7 +282,8 @@ def record_service_presence(request):
import re
o_date = re.search(r'/([^\/]+?)$', request.META.get('HTTP_REFERER'))
if o_date:
overrided_date = o_date.group(1)
overrided_date = re.sub(r'(%20)',' ', o_date.group(1))
# rid = 0 => C'est un rattrapage, sur le service
if sid > 0 and stid > 0:
# Add member to service and take presence into account
......@@ -289,7 +291,6 @@ def record_service_presence(request):
if res['rattrapage'] is True:
res['update'] = 'ok'
else:
if (CagetteServices.registration_done(rid, overrided_date) is True):
res['update'] = 'ok'
else:
......@@ -325,8 +326,8 @@ def easy_validate_shift_presence(request):
else:
return JsonResponse(res, safe=False)
def record_absences(request):
return JsonResponse({'res': CagetteServices.record_absences()})
def record_absences(request, date):
return JsonResponse({'res': CagetteServices.record_absences(date)})
def close_ftop_service(request):
"""Close the closest past FTOP service"""
......
......@@ -11,38 +11,37 @@ class CagetteMembersSpace(models.Model):
"""Init with odoo id."""
self.o_api = OdooAPI()
def get_points_history(self, partner_id, limit, offset, date_from, shift_type):
""" Get partner points history with related shift registration if needed """
cond = [
['partner_id', '=', partner_id],
['type', '=', shift_type],
['create_date', '>', date_from],
['point_qty', '!=', 0]
]
f = ['create_date', 'create_uid', 'shift_id', 'name', 'point_qty']
res = self.o_api.search_read('shift.counter.event', cond, f, limit=limit, offset=offset,
order='create_date DESC')
# Get related data from shift.registration
shift_ids = []
for item in res:
item['is_late'] = False # So every item has the attribute
if item['shift_id'] is not False:
shift_ids.append(item['shift_id'][0])
cond = [['shift_id', 'in', shift_ids]]
f = ['is_late', 'shift_id']
res_shift_registration = self.o_api.search_read('shift.registration', cond, f)
for registration_item in res_shift_registration:
for shift_counter_item in res:
if (shift_counter_item['shift_id'] is not False
and shift_counter_item['shift_id'] == registration_item['shift_id']):
shift_counter_item['is_late'] = registration_item['is_late']
break
def get_shifts_history(self, partner_id, limit, offset, date_from):
""" Get partner shifts history """
res = {}
today = str(datetime.date.today())
try:
cond = [
['partner_id', '=', partner_id],
['create_date', '>', date_from],
['date_begin', '<', today],
['state', '!=', 'draft'],
['state', '!=', 'open'],
['state', '!=', 'waiting'],
['state', '!=', 'replaced'],
['state', '!=', 'replacing'],
]
f = ['create_date', 'shift_id', 'name', 'state', 'is_late', 'is_makeup']
marshal_none_error = 'cannot marshal None unless allow_none is enabled'
try:
res = self.o_api.search_read('shift.registration', cond, f, limit=limit, offset=offset,
order='create_date DESC')
except Exception as e:
if not (marshal_none_error in str(e)):
res['error'] = repr(e)
coop_logger.error(res['error'] + ' : %s', str(payment_id))
else:
res = []
except Exception as e:
print(str(e))
return res
\ No newline at end of file
......@@ -34,8 +34,15 @@
display: none;
}
@media screen and (max-width: 768px) {
/* When the screen is less than 768 pixels wide, hide all links, except for the first one ("Home"). Show the link that contains should open and close the topnav (.icon) */
.pairs_info {
background-color: #00a573;
color: white;
padding: 1.5rem 1.2rem;
display: none;
}
@media screen and (max-width: 992px) {
/* When the screen is less than 992 pixels wide, hide all links, except for the first one ("Home"). Show the link that contains should open and close the topnav (.icon) */
.topnav a:not(:first-child) {display: none;}
.topnav a.icon {
float: right;
......
......@@ -7,6 +7,7 @@
#my_info_content {
display: flex;
flex-direction: column;
margin: 2rem 0;
}
.my_info_line {
......@@ -40,14 +41,13 @@
flex-direction: column;
}
.pairs_info {
background-color: #00a573;
color: white;
padding: 1.5rem 1.2rem;
margin: 2rem 0;
.member_phone_area {
display: flex;
flex-direction: column;
gap: 10px;
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 992px) {
#my_info {
font-size: 1.7rem;
}
......
......@@ -35,7 +35,14 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>th:first-child:before {
background-color: white;
font-weight: bold;
border: none;
font-size: 2rem;
font-size: 1.6rem;
height: 16px;
width: 16px;
border-radius: 2em;
}
@media screen {
}
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before {
......@@ -43,7 +50,10 @@ table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before {
background-color: white;
font-weight: bold;
border: none;
font-size: 2rem;;
font-size: 1.6rem;
height: 16px;
width: 16px;
border-radius: 2em;
}
.loading-more-history {
......
......@@ -24,7 +24,7 @@
text-align: center;
width: 50%;
}
@media screen and (max-width:768px) {
@media screen and (max-width:992px) {
#suspended_cant_have_delay_content {
align-items: center;
text-align: center;
......@@ -39,7 +39,7 @@
justify-content: space-between;
}
@media screen and (max-width:768px) {
@media screen and (max-width:992px) {
#calendar_top_info {
display: flex;
flex-direction: column;
......@@ -54,9 +54,10 @@
display: none;
width: min-content;
max-width: 100%;
white-space: nowrap;
}
@media screen and (max-width:768px) {
@media screen and (max-width:992px) {
#partner_shifts_list {
display: flex;
flex-direction: column;
......@@ -96,7 +97,7 @@
margin-right: 3px;
}
@media screen and (max-width:768px) {
@media screen and (max-width:992px) {
.select_makeups_message_block {
display: block;
}
......@@ -113,7 +114,7 @@
display: none;
}
@media screen and (max-width:768px) {
@media screen and (max-width:992px) {
#calendar {
display: none;
}
......@@ -168,7 +169,7 @@
padding: 0 !important;
}
@media screen and (max-width:768px) {
@media screen and (max-width:992px) {
.example-event {
margin: 2rem auto 0.5rem auto;
}
......
......@@ -13,6 +13,12 @@ body {
flex-wrap: wrap;
}
@media screen and (max-width: 992px) {
.tiles_container {
flex-direction: column;
}
}
.tile {
flex: 1 0 45%;
display: flex;
......@@ -39,15 +45,10 @@ body {
justify-content: center;
align-items: center;
border-bottom: 1px solid #e7e9ed;
font-size: 2.7rem;
font-size: 2.4rem;
padding: 2rem 0;
width: 80%;
}
@media screen and (max-width: 768px) {
.tile_title {
font-size: 2.5rem;
}
}
.tile_content {
position: relative;
......@@ -58,7 +59,11 @@ body {
}
#home_tile_services_exchange .tile_content {
justify-content: center;
height: 100%;
flex-direction: column;
align-items: center;
text-align: center;
}
......@@ -109,6 +114,10 @@ body {
position: relative;
}
#home_tile_my_info .tile_content {
margin: 2rem 0;
}
.tile_icon {
margin-right: 15px;
color: #00a573;
......@@ -118,11 +127,17 @@ body {
height: 100%;
flex-direction: column;
align-items: center;
font-size: 2.2rem;
font-size: 2rem;
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 992px) {
#home_tile_my_info .tile_content {
font-size: 1.9rem;
font-size: 1.7rem;
}
}
@media screen and (max-width: 380px) {
#home_tile_my_info .tile_content {
font-size: 1.6rem !important;
}
}
......@@ -130,24 +145,29 @@ body {
font-weight: bold;
}
.member_status_text_container {
margin-bottom: 5px;
}
#member_status_action {
margin-bottom: 20px;
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 992px) {
#member_status_action {
margin-top: 5px;
margin-bottom: 10px;
}
}
.choose_makeups {
display: none;
font-size: 1.8rem;
font-size: 1.5rem;
}
.unsuscribed_form_link {
display: none;
text-decoration: none;
font-size: 1.7rem;
font-size: 1.5rem;
}
.unsuscribed_form_link:hover {
text-decoration: none;
......@@ -167,17 +187,38 @@ body {
color: #d9534f;
}
.member_shift_name_area {
margin-top: 10px;
}
.member_shift_name_area,
.member_coop_number_area {
margin-bottom: 10px;
}
@media screen and (max-width: 768px) {
.member_shift_name_area,
.member_coop_number_area,
.member_associated_partner_area {
font-size: 1.9rem;
line-height: 1.3;
}
@media screen and (max-width: 380px) {
.member_shift_name_area,
.member_coop_number_area,
.member_associated_partner_area {
font-size: 1.6rem;
}
}
@media screen and (max-width: 992px) {
.member_shift_name_area,
.member_coop_number_area {
.member_coop_number_area,
.member_associated_partner_area {
display: flex;
flex-direction: column;
align-items: center;
font-size: 1.7rem;
}
}
......@@ -268,7 +309,7 @@ body {
}
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 992px) {
#shop_info_content {
flex-direction: column;
}
......
......@@ -47,4 +47,8 @@ $(document).ready(function() {
$('#nav_calendar').on('click', () => {
toggleHeader();
});
if (partner_data.is_associated_people === "True") {
$(".pairs_info").show();
}
});
......@@ -25,7 +25,7 @@ function request_delay() {
},
success: function() {
partner_data.cooperative_state = 'delay';
partner_data.date_delay_stop = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
partner_data.date_delay_stop = today_plus_six_month.getFullYear()+'-'+(today_plus_six_month.getMonth()+1)+'-'+today_plus_six_month.getDate();
resolve();
},
......@@ -97,6 +97,18 @@ function init_home() {
});
$("#go_to_forms").prop("href", forms_link);
if (partner_data.is_in_association === false) {
$("#home .member_associated_partner_area").hide();
} else {
if (partner_data.is_associated_people === "True") {
$(".member_associated_partner").text(partner_data.parent_name);
} else if (partner_data.associated_partner_id !== "False") {
$(".member_associated_partner").text(partner_data.associated_partner_name);
}
}
// TODO vérif tile my info avec données binomes + rattrapage et délai
// Init my info tile
init_my_info_data();
......
......@@ -3,10 +3,14 @@ function init_my_info() {
$(".member_email").text(partner_data.email);
if (partner_data.is_associated_people === "False") {
if (partner_data.is_in_association === false) {
$("#attached_info_area").hide();
} else {
}
if (partner_data.is_associated_people === "True") {
$(".attached_partner_name").text(partner_data.parent_name);
} else if (partner_data.associated_partner_id !== "False") {
$(".attached_partner_name").text(partner_data.associated_partner_name);
}
if (partner_data.street !== "") {
......@@ -22,17 +26,22 @@ function init_my_info() {
$(".member_address_line").hide();
}
if (partner_data.mobile !== "") {
if (partner_data.mobile !== "" && partner_data.mobile !== "False" && partner_data.mobile !== false && partner_data.mobile !== null) {
$(".member_mobile")
.append(partner_data.mobile);
} else {
$(".member_mobile_line").hide();
$(".member_mobile").hide();
}
if (partner_data.phone !== "") {
if (partner_data.phone !== "" && partner_data.phone !== "False" && partner_data.phone !== false && partner_data.phone !== null) {
$(".member_phone")
.append(partner_data.phone);
} else {
$(".member_phone").hide();
}
if ($(".member_mobile").text() === "" && $(".member_phone").text() === "") {
$(".member_phone_line").hide();
}
}
\ No newline at end of file
......@@ -9,13 +9,12 @@ function load_partner_history(offset = 0) {
return new Promise((resolve) => {
$.ajax({
type: 'GET',
url: "/members_space/get_points_history",
url: "/members_space/get_shifts_history",
data: {
partner_id: partner_data.concerned_partner_id,
verif_token: partner_data.verif_token,
limit: history_items_limit,
offset: offset,
shift_type: (partner_data.in_ftop_team === "True") ? "ftop" : "standard"
offset: offset
},
dataType:"json",
traditional: true,
......@@ -49,36 +48,18 @@ function prepare_server_data(data) {
res = [];
for (history_item of data) {
// Date formating
let datetime_shift_start = new Date(history_item.create_date);
let f_date_shift_start = datetime_shift_start.toLocaleDateString("fr-fr", date_options);
f_date_shift_start = f_date_shift_start.charAt(0).toUpperCase() + f_date_shift_start.slice(1);
history_item.movement_date = f_date_shift_start + " - " + datetime_shift_start.toLocaleTimeString("fr-fr", time_options);
// Text replacements
history_item.name = (history_item.name === "Clôturer le service") ? "Décompte 28j" : history_item.name;//Clôlturer le service
history_item.name = (history_item.name === "Rattrapage") ? "Absence" : history_item.name;
if (history_item.name === "Clôturer le service" || history_item.name === "Clôlturer le service") {
history_item.name = "Décompte 28j";
} else if (history_item.name === "Rattrapage") {
history_item.name = "Absence";
} else if (history_item.name === "Présent" && history_item.is_late != false) {
history_item.name = "Retard";
}
history_item.created_by = history_item.create_uid[1];
if (history_item.created_by === "Administrator") {
history_item.created_by = "Administrateur";
} else if (history_item.created_by === "api") {
history_item.created_by = "Système";
history_item.details = '';
if (history_item.state === 'excused' || history_item.state === 'absent') {
history_item.details = "Absent";
} else if (history_item.state === 'done' && history_item.is_late != false) {
history_item.details = "Présent (En Retard)";
} else if (history_item.state === 'done') {
history_item.details = "Présent";
} else if (history_item.state === 'cancel') {
history_item.details = "Annulé";
}
history_item.shift_name = (history_item.shift_id === false) ? '' : history_item.shift_id[1];
// if Present && is_late -> Absent
}
return data;
......@@ -99,18 +80,14 @@ function init_history() {
data: partner_history,
columns: [
{
data: "movement_date",
title: `Date`,
responsivePriority: 1
},
{
data: "shift_name",
title: "Service"
title: "<spans class='dt-body-center'>Service</span>",
width: "60%"
},
{
data: "name",
data: "details",
title: "Détails",
responsivePriority: 3
className: "tablet-l desktop"
}
],
iDisplayLength: -1,
......@@ -126,7 +103,7 @@ function init_history() {
$(row).addClass('row_partner_ok');
} else if (cell.text() === "Retard") {
$(row).addClass('row_partner_late');
} else if (cell.text() === "Absence") {
} else if (cell.text() === "Absent") {
$(row).addClass('row_partner_absent');
}
}
......
......@@ -32,7 +32,7 @@ function add_or_change_shift(new_shift_id) {
tData = 'idNewShift=' + new_shift_id
+'&idPartner=' + partner_data.partner_id
+ '&in_ftop_team=' + partner_data.in_ftop_team
+ '&shift_type=' + partner_data.shift_type
+ '&verif_token=' + partner_data.verif_token;
if (selected_shift === null) {
......
......@@ -15,7 +15,7 @@ const possible_cooperative_state = {
exempted: "Exempté.e",
alert: "En alerte",
up_to_date: "À jour",
unsubscribed: "Désinscrit.e",
unsubscribed: "Désinscrit.e des créneaux",
delay: "En délai"
};
......@@ -68,6 +68,10 @@ function goto(page) {
/**
* Define which html content to load from server depending on the window location
*
* WARNING: For the routing system to work,
* public urls (those the users will see & navigate to) must be different than the server urls used to fetch resources
* (ex: public url: /members_space/mes-info ; server url: /members_space/my_info)
*/
function update_dom() {
$(".nav_item").removeClass('active');
......@@ -150,6 +154,11 @@ function init_my_info_data() {
$(".member_shift_name").text(partner_data.regular_shift_name);
let pns = partner_data.name.split(" - ");
let name = pns.length > 1 ? pns[1] : pns[0];
$(".member_name").text(name);
// Status related
$(".member_status")
.text(possible_cooperative_state[partner_data.cooperative_state])
......@@ -214,6 +223,14 @@ $(document).ready(function() {
? partner_data.parent_id
: partner_data.partner_id;
partner_data.is_in_association =
partner_data.is_associated_people === "True" || partner_data.associated_partner_id !== "False";
// For associated people, their parent name is attached in their display name
let partner_name_split = partner_data.name.split(', ');
partner_data.name = partner_name_split[partner_name_split.length - 1];
base_location = (app_env === 'dev') ? '/members_space/' : '/';
update_dom();
......
......@@ -5,11 +5,11 @@ from . import views
urlpatterns = [
url(r'^$', views.index),
url(r'^homepage$', views.home),
url(r'^homepage$', views.home), # These endpoints must be different than in-app url
url(r'^my_info$', views.my_info),
url(r'^my_shifts$', views.my_shifts),
url(r'^shifts_exchange$', views.shifts_exchange),
url(r'^no_content$', views.no_content),
url(r'^get_points_history$', views.get_points_history),
url('/*$', views.index),
url(r'^get_shifts_history$', views.get_shifts_history),
url('/*$', views.index), # Urls unknown from the server will redirect to index
]
......@@ -39,6 +39,7 @@ def index(request, exception=None):
if 'msg' in credentials:
context['msg'] = credentials['msg']
context['password_placeholder'] = 'Naissance (jjmmaaaa)'
context['is_member_space'] = True
elif ('validation_state' in credentials) and credentials['validation_state'] == 'waiting_validation_member':
# First connection, until the member validated his account
template = loader.get_template('members/validation_coop.html')
......@@ -76,6 +77,7 @@ def index(request, exception=None):
partner_id = credentials['id']
cs = CagetteShift()
partnerData = cs.get_data_partner(partner_id)
if 'create_date' in partnerData:
......@@ -89,12 +91,23 @@ def index(request, exception=None):
except:
pass
# look for parent for associated partner
if partnerData["parent_id"] is not False:
partnerData["parent_name"] = partnerData["parent_id"][1]
partnerData["parent_id"] = partnerData["parent_id"][0]
else:
partnerData["parent_name"] = False
# look for associated partner for parents
cm = CagetteMember(partner_id)
associated_partner = cm.search_associated_people()
partnerData["associated_partner_id"] = False if associated_partner is None else associated_partner["id"]
partnerData["associated_partner_name"] = False if associated_partner is None else associated_partner["name"]
if (associated_partner is not None and partnerData["associated_partner_name"].find(str(associated_partner["barcode_base"])) == -1):
partnerData["associated_partner_name"] = str(associated_partner["barcode_base"]) + ' - ' + partnerData["associated_partner_name"]
partnerData['can_have_delay'] = cs.member_can_have_delay(int(partner_id))
context['partnerData'] = partnerData
......@@ -125,6 +138,12 @@ def index(request, exception=None):
return _get_response_according_to_credentials(request, credentials, context, template)
def home(request):
"""
Endpoint the front-end will call to load the "home" page.
Consequently, the front-end url should be unknown from the server so the user is redirected to the index,
then the front-end index will call this endpoint to load the home page
"""
template = loader.get_template('members_space/home.html')
context = {
'title': 'Espace Membres',
......@@ -138,6 +157,7 @@ def home(request):
return HttpResponse(template.render(context, request))
def my_info(request):
""" Endpoint the front-end will call to load the "My info" page. """
template = loader.get_template('members_space/my_info.html')
context = {
'title': 'Mes Infos',
......@@ -145,6 +165,7 @@ def my_info(request):
return HttpResponse(template.render(context, request))
def my_shifts(request):
""" Endpoint the front-end will call to load the "My shifts" page. """
template = loader.get_template('members_space/my_shifts.html')
context = {
'title': 'Mes Services',
......@@ -152,6 +173,7 @@ def my_shifts(request):
return HttpResponse(template.render(context, request))
def shifts_exchange(request):
""" Endpoint the front-end will call to load the "Shifts exchange" page. """
template = loader.get_template('members_space/shifts_exchange.html')
context = {
'title': 'Échange de Services',
......@@ -159,13 +181,14 @@ def shifts_exchange(request):
return HttpResponse(template.render(context, request))
def no_content(request):
""" Endpoint the front-end will call to load the "No content" page. """
template = loader.get_template('members_space/no_content.html')
context = {
'title': 'Contenu non trouvé',
}
return HttpResponse(template.render(context, request))
def get_points_history(request):
def get_shifts_history(request):
res = {}
partner_id = int(request.GET.get('partner_id'))
......@@ -173,8 +196,7 @@ def get_points_history(request):
limit = int(request.GET.get('limit'))
offset = int(request.GET.get('offset'))
shift_type = request.GET.get('shift_type')
date_from = getattr(settings, 'START_DATE_FOR_POINTS_HISTORY', '2018-01-01')
res["data"] = m.get_points_history(partner_id, limit, offset, date_from, shift_type)
date_from = getattr(settings, 'START_DATE_FOR_SHIFTS_HISTORY', '2018-01-01')
res["data"] = m.get_shifts_history(partner_id, limit, offset, date_from)
return JsonResponse(res)
\ No newline at end of file
......@@ -264,6 +264,15 @@
(if not set, 60 minutes is the default)
- ENTRANCE_VALIDATE_PRESENCE_MESSAGE = """
<div class="explanations">
Ta présence a bien été validée ! Merci de te diriger au fond du magasin pour le lancement du créneau !
</div>
Ton prochain service <span class="service_verb">est prévu</span> le <span class="next_shift"></span>
"""
(La Cagette message, where no point data is displayed)
### Member space
- EM_URL = ''
......@@ -352,7 +361,7 @@
Should be set to False by default if parameter not set
- START_DATE_FOR_POINTS_HISTORY = "2018-01-01"
- START_DATE_FOR_SHIFTS_HISTORY = "2018-01-01"
......
......@@ -21,5 +21,7 @@ def custom_css(request):
def context_setting(request):
"""adding settings variable to context (can be overloaded in views)."""
context = {'odoo': settings.ODOO['url'], 'app_env': getattr(settings, 'APP_ENV', "prod") }
context = {'odoo': settings.ODOO['url'],
'app_env': getattr(settings, 'APP_ENV', "prod"),
'company_code': getattr(settings, 'COMPANY_CODE', '')}
return context
\ No newline at end of file
......@@ -36,7 +36,7 @@ class CagetteShift(models.Model):
fields = ['display_name', 'display_std_points',
'shift_type', 'date_alert_stop', 'date_delay_stop', 'extension_ids',
'cooperative_state', 'final_standard_point', 'create_date',
'final_ftop_point', 'in_ftop_team', 'leave_ids', 'makeups_to_do', 'barcode_base',
'final_ftop_point', 'shift_type', 'leave_ids', 'makeups_to_do', 'barcode_base',
'street', 'street2 ,' 'zip', 'city', 'mobile', 'phone', 'email',
'is_associated_people', 'parent_id']
partnerData = self.o_api.search_read('res.partner', cond, fields, 1)
......@@ -89,6 +89,15 @@ class CagetteShift(models.Model):
shiftData = self.o_api.search_read('shift.registration', cond, fields, order ="date_begin ASC")
return shiftData
def shift_is_makeup(self, id):
"""vérifie si une shift est un rattrapage"""
fields = ["is_makeup", "id"]
cond = [['id', '=', id]]
shiftData = self.o_api.search_read('shift.registration', cond, fields)
return shiftData[0]["is_makeup"]
def get_shift_calendar(self, id, start, end):
"""Récupère les shifts à partir de maintenant pour le calendier"""
cond = [['date_begin', '>', datetime.datetime.now().isoformat()],
......@@ -120,14 +129,14 @@ class CagetteShift(models.Model):
fields = ['stop_date', 'id', 'start_date']
return self.o_api.search_read('shift.leave', cond, fields)
def get_shift_ticket(self,idShift, in_ftop_team):
def get_shift_ticket(self,idShift, shift_type):
"""Récupérer le shift_ticket suivant le membre et flotant ou pas"""
if getattr(settings, 'USE_STANDARD_SHIFT', True) == False:
in_ftop_team = "True"
shift_type = "ftop"
fields = ['shift_ticket_ids']
cond = [['id', "=", idShift]]
listeTicket = self.o_api.search_read('shift.shift', cond, fields)
if in_ftop_team == "True":
if shift_type == "ftop":
return listeTicket[0]['shift_ticket_ids'][1]
else:
return listeTicket[0]['shift_ticket_ids'][0]
......@@ -137,13 +146,14 @@ class CagetteShift(models.Model):
st_r_id = False
try:
shift_type = "standard"
if data['in_ftop_team'] == "True" or getattr(settings, 'USE_STANDARD_SHIFT', True) == False:
if data['shift_type'] == "ftop" or getattr(settings, 'USE_STANDARD_SHIFT', True) == False:
shift_type = "ftop"
fieldsDatas = { "partner_id": data['idPartner'],
"shift_id": data['idShift'],
"shift_ticket_id": self.get_shift_ticket(data['idShift'], data['in_ftop_team']),
"shift_ticket_id": self.get_shift_ticket(data['idShift'], data['shift_type']),
"shift_type": shift_type,
"origin": 'memberspace',
"is_makeup": data['is_makeup'],
"state": 'open'}
st_r_id = self.o_api.create('shift.registration', fieldsDatas)
......
......@@ -57,7 +57,7 @@ function loadShiftPartner(partner_id) {
$('#shift_msg').remove();
$('#partnerData').append('<div id="shift_msg"></div>');
if (dataPartner.in_ftop_team == "True" || listeShiftPartner.length > 0) {
if (dataPartner.shift_type == "ftop" || listeShiftPartner.length > 0) {
// ftop, no shift planned
if (listeShiftPartner.length == 0) {
var date = new Date(dataPartner.next_regular_shift_date);
......@@ -79,7 +79,7 @@ function loadShiftPartner(partner_id) {
// Set DOM for partner's shifts and shift message for ftops
iniListShift(listeShiftPartner, true);
if (dataPartner.in_ftop_team == "True") {
if (dataPartner.shift_type == "ftop") {
$('#shift_msg').append("<br /><strong>Je peux choisir d'autres services pour les mois à venir ou échanger un de ceux de la liste.</strong>");
}
}
......@@ -92,7 +92,7 @@ function changeShift(idOldRegister, idNewShift) {
if (is_time_to('change_shift')) {
openModal(); // loading on
tData = 'idNewShift=' + idNewShift +'&idPartner=' + dataPartner.partner_id + '&in_ftop_team=' + dataPartner.in_ftop_team + '&verif_token=' + dataPartner.verif_token;
tData = 'idNewShift=' + idNewShift +'&idPartner=' + dataPartner.partner_id + '&shift_type=' + dataPartner.shift_type + '&verif_token=' + dataPartner.verif_token;
if (idOldRegister == "") {
tUrl = '/shifts/add_shift';
} else {
......@@ -161,7 +161,7 @@ function canMakeExchange() {
var answer = false;
// Set the partner's limit date (after which he'll loose a point)
if (dataPartner.dateProlonge != "False" || dataPartner.final_standard_point < 0 || dataPartner.in_ftop_team == "True") {
if (dataPartner.dateProlonge != "False" || dataPartner.final_standard_point < 0 || dataPartner.shift_type == "ftop") {
var dateProlonge = new Date(dataPartner.dateProlonge);
var dateNextRegularShift = new Date(dataPartner.next_regular_shift_date);
......@@ -170,7 +170,7 @@ function canMakeExchange() {
// For ABCD : the limit date is end of alert
var dateEndAlert = new Date(dataPartner.date_alert_stop);
if (dataPartner.in_ftop_team == "False" && limitDate < dateEndAlert) {
if (dataPartner.shift_type == "ftop" && limitDate < dateEndAlert) {
limitDate = dateEndAlert;
}
......@@ -204,7 +204,7 @@ function canMakeExchange() {
});
// Allow exchange if points >= 0 or he already has enough services booked before the limit date
var partner_points = dataPartner.in_ftop_team == "True" ? dataPartner.final_ftop_point : dataPartner.final_standard_point;
var partner_points = dataPartner.shift_type == "ftop" ? dataPartner.final_ftop_point : dataPartner.final_standard_point;
if (partner_points >= 0 || shifts_before_limit >= 1) {
answer = true;
......@@ -216,7 +216,7 @@ function canMakeExchange() {
}
// ftop can always exchange service
if (dataPartner.in_ftop_team == "True") {
if (dataPartner.shift_type == "ftop") {
answer = true;
}
}
......@@ -230,7 +230,7 @@ Génère le message à afficher lorsque le coop doit faire un rattrapage.
Pour les volants, chaque service compte comme un rattrapage.
*/
function addMakeUpMsg() {
var partner_points = dataPartner.in_ftop_team == "True" ? dataPartner.final_ftop_point : dataPartner.final_standard_point;
var partner_points = dataPartner.shift_type == "ftop" ? dataPartner.final_ftop_point : dataPartner.final_standard_point;
let shifts_before_limit = 0;
// Calcul du nombre de rattrapages à faire
......@@ -281,7 +281,7 @@ function addMakeUpMsg() {
}
// Si le membre est un volant
if (dataPartner.in_ftop_team == "True") {
if (dataPartner.shift_type == "ftop") {
msg = "Je dois faire " + make_up_nb + " service";
if (make_up_nb > 1) msg += "s";
if (non_regular_shifts.length > 0) msg += " en plus";
......@@ -320,7 +320,7 @@ function canAddShift(date_new_shift) {
var answer = false;
// If partner is ftop (ftop = volant)
if (dataPartner["in_ftop_team"] == "True") {
if (dataPartner["shift_type"] == "ftop") {
// If points >= 0 : can register to any shift
if (dataPartner.final_ftop_point >= 0) {
answer = true;
......@@ -414,7 +414,7 @@ $(document).ready(function() {
loadShiftPartner(dataPartner.partner_id);
// Display information depending on partner's type and state
if (dataPartner.in_ftop_team == "True") {
if (dataPartner.shift_type == "ftop") {
$('div.intro div h2').text("Bienvenue dans le système de choix et d'échange de services");
$('.additionnal_intro_data').text(' ou en choisir un nouveau');
......@@ -543,7 +543,7 @@ $(document).ready(function() {
// For partners who can't add a shift as it is
if (!can_add_shift) {
// Partners who could ask for a delay
if (dataPartner.in_ftop_team == "True" || dataPartner.in_ftop_team == "False" && dateShiftNew > limitDate) {
if (dataPartner.shift_type == "ftop" || dateShiftNew > limitDate) {
// Member can ask for 6 delays, which is 24 weeks after entering alert status
// 'date_alert_stop' field is begining of alert + 4 weeks
let date_end_alert = new Date(dataPartner.date_alert_stop);
......
......@@ -178,10 +178,12 @@ def change_shift(request):
if 'idNewShift' in request.POST and 'idOldShift' in request.POST:
idOldShift = request.POST['idOldShift']
listRegister = [int(request.POST['idRegister'])]
data = {
"idPartner": int(request.POST['idPartner']),
"idShift":int(request.POST['idNewShift']),
"in_ftop_team":request.POST['in_ftop_team']
"idShift": int(request.POST['idNewShift']),
"shift_type": request.POST['shift_type'],
"is_makeup": cs.shift_is_makeup(idOldShift)
}
should_block_service_exchange = getattr(settings, 'BLOCK_SERVICE_EXCHANGE_24H_BEFORE', False)
......@@ -228,7 +230,8 @@ def add_shift(request):
data = {
"idPartner": int(request.POST['idPartner']),
"idShift":int(request.POST['idNewShift']),
"in_ftop_team":request.POST['in_ftop_team']
"shift_type":request.POST['shift_type'],
"is_makeup":True
}
#Insertion du nouveau shift
......
......@@ -188,6 +188,9 @@
<h1 class="col-6 txtcenter">Bon service !</h1>
<section class="col-6 txtcenter">
<span class="member_name"></span><br />
{% if ENTRANCE_VALIDATE_PRESENCE_MESSAGE != '' %}
{{ENTRANCE_VALIDATE_PRESENCE_MESSAGE|safe}}
{% else %}
Votre présence est bien enregistrée ! <br />
<div class="compteur">
Votre compteur est dorénavant à <span class="points"></span> point(s).<br />
......@@ -200,6 +203,7 @@
</div>
</div>
Votre prochain service <span class="service_verb">est prévu</span> le <span class="next_shift"></span>
{% endif %}
</section>
<div class="col-2"></div>
<a class="btn col-2" data-next="first_page">Coopérateur suivant</a>
......
......@@ -24,6 +24,14 @@
<i class="fa fa-bars"></i>
</a>
</div>
<div class="pairs_info">
<span>
<i class="fas fa-exclamation-circle"></i> Je suis en binôme.
Toutes les actions (changement de service, choix d'un rattrapage...)
ne sont faisables que sur l'espace membre du <b>binôme principal</b>.
Dans mon espace membre, les infos ne sont visibles qu'en lecture seule.
</span>
</div>
<script type="text/javascript" src="{% static 'js/members-space-header.js' %}"></script>
{% endblock %}
......@@ -6,10 +6,11 @@
<div class="tile high_tile" id="home_tile_my_info">
<div class="tile_title">
<i class="fas fa-user tile_icon"></i>
Mes Infos
<span class="member_info member_name"></span>
</div>
<div class="tile_content">
<p>Mon statut : <span class="member_info member_status"></span></p>
{# <p><span class="member_info member_name"></span></p> #}
<p class="member_status_text_container">Mon statut : <span class="member_info member_status"></span></p>
<div class="delay_date_stop_container">
( jusqu'au <span class="delay_date_stop"></span> )
</div>
......@@ -29,6 +30,10 @@
<span>Mon numéro de coop : </span>
<span class="member_coop_number member_info"></span>
</div>
<div class="member_associated_partner_area">
<span>Je suis en binôme avec : </span>
<span class="member_associated_partner member_info"></span>
</div>
</div>
<div id="see_more_info">
<a href="#" id="see_more_info_link">Voir plus ></a>
......@@ -57,6 +62,9 @@
Échange de services
</div>
<div class="tile_content">
<div>
Un empêchement ? J'anticipe et déplace mes services jusqu'à 24h avant leur début !
</div>
<div class="home_link_button_area">
<button type="button" class="btn--primary home_link_button" id="go_to_shifts_calendar">
Accéder au calendrier d'échange de services
......@@ -74,8 +82,7 @@
<a
href="javascript:void(0);"
target="_blank"
type="button"
class="btn--primary home_link_button"
class="btn--primary home_link_button"
id="go_to_forms"
>
Accéder aux formulaires
......@@ -107,4 +114,4 @@
</div>
</div>
</div>
</div>
\ No newline at end of file
</div>
......@@ -82,7 +82,7 @@
var partner_data = {
"partner_id":"{{partnerData.id}}",
"name":"{{partnerData.display_name}}",
"in_ftop_team":"{{partnerData.in_ftop_team}}",
"shift_type":"{{partnerData.shift_type}}",
"date_delay_stop":"{{partnerData.date_delay_stop}}",
"cooperative_state":"{{partnerData.cooperative_state}}",
"regular_shift_name":"{{partnerData.regular_shift_name}}",
......@@ -99,6 +99,8 @@
"is_associated_people" : "{{partnerData.is_associated_people}}",
"parent_id" : "{{partnerData.parent_id}}",
"parent_name" : "{{partnerData.parent_name}}",
"associated_partner_id" : "{{partnerData.associated_partner_id}}",
"associated_partner_name" : "{{partnerData.associated_partner_name}}",
"verif_token" : "{{partnerData.verif_token}}",
}
</script>
......
......@@ -4,6 +4,10 @@
</div>
<div class="tiles_container">
<div class="tile full_width_tile" id="my_info_area">
<div class="tile_title">
<i class="fas fa-user tile_icon"></i>
<span class="member_info member_name"></span>
</div>
<div class="tile_content" id="my_info_content">
<div class="my_info_line">
<div class="my_info_line_left">
......@@ -58,20 +62,13 @@
<span class="member_address member_info"></span>
</div>
</div>
<div class="my_info_line member_mobile_line">
<div class="my_info_line_left">
Téléphone mobile
</div>
<div class="my_info_line_right member_mobile_area">
<span class="member_mobile member_info"></span>
</div>
</div>
<div class="my_info_line member_phone_line">
<div class="my_info_line_left">
Téléphone fixe
Téléphone
</div>
<div class="my_info_line_right member_phone_area">
<span class="member_phone member_info"></span>
<span class="member_mobile member_info"></span>
</div>
</div>
</div>
......@@ -89,16 +86,6 @@
<span class="attached_partner_name member_info"></span>
</div>
</div>
<div class="my_info_line pairs_info">
<span>
Attention : Toutes les actions (changement de service, choix d'un rattrapage...)
ne sont faisables que sur l'espace membre du <b>binôme principal</b>.
</span>
<br>
<span>
Dans mon espace membre, les infos ne sont visibles qu'en lecture seule.
</span>
</div>
</div>
</div>
</div>
......
......@@ -15,7 +15,7 @@
</div>
<div class="tile full_width_tile" id="history_area">
<div class="tile_title">
Historique des mouvements de points
Historique de mes services
</div>
<div class="loading-history">
<i class="fas fa-spinner fa-spin fa-lg"></i>
......
......@@ -13,8 +13,8 @@
</div>
<div id="suspended_content" class="shifts_exchange_page_content">
<h3>
J'ai <span class="makeups_nb"></span> rattrapages à effectuer, je dois les sélectionner pour pouvoir refaire mes courses.
J'ai 6 mois de délai pour les rattraper.
J'ai <span class="makeups_nb"></span> rattrapage(s) à effectuer, je dois le(s) sélectionner pour pouvoir refaire mes courses.
J'ai 6 mois de délai pour le(s) rattraper.
</h3>
<h3>
Si besoin, je peux contacter le Bureau des membres via la rubrique "J'ai une demande" pour expliquer ma situation.
......
......@@ -16,7 +16,7 @@
dataPartner = {
"partner_id":"{{partnerData.id}}",
"name":"{{partnerData.display_name}}",
"in_ftop_team":"{{partnerData.in_ftop_team}}",
"shift_type":"{{partnerData.shift_type}}",
"final_standard_point":"{{partnerData.final_standard_point}}",
"final_ftop_point":"{{partnerData.final_ftop_point}}",
"dateProlonge":"{{partnerData.date_delay_stop}}",
......
......@@ -35,4 +35,19 @@
}
</script>
</div>
<script>
// For the members space, reset url to home when accessing connect page
const is_member_space = '{{is_member_space}}';
if (is_member_space === "True") {
var app_env = '{{app_env}}';
var base_location = (app_env === 'dev') ? '/members_space/' : '/';
if (window.location.pathname === base_location) {
history.pushState({}, '', 'home');
} else {
history.replaceState({}, '', 'home');
}
}
</script>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment