mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-25 18:44:23 +00:00
Merge pull request #477 from imperosol/eboutic
Refonte de la boutique en ligne
This commit is contained in:
commit
d3c115e3f9
1
.mailmap
1
.mailmap
@ -16,3 +16,4 @@ Zar <antoine.charmeau@utbm.fr> <antoine.charmeau@laposte.net>
|
|||||||
root <root@localhost.localdomain>
|
root <root@localhost.localdomain>
|
||||||
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
|
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
|
||||||
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>
|
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>
|
||||||
|
Maréchal <thgirod@hotmail.com>
|
@ -505,6 +505,8 @@ Welcome to the wiki page!
|
|||||||
refound.save()
|
refound.save()
|
||||||
|
|
||||||
# Counters
|
# Counters
|
||||||
|
subscribers = Group.objects.get(name="Subscribers")
|
||||||
|
old_subscribers = Group.objects.get(name="Old subscribers")
|
||||||
Customer(user=skia, account_id="6568j", amount=0).save()
|
Customer(user=skia, account_id="6568j", amount=0).save()
|
||||||
Customer(user=r, account_id="4000k", amount=0).save()
|
Customer(user=r, account_id="4000k", amount=0).save()
|
||||||
p = ProductType(name="Bières bouteilles")
|
p = ProductType(name="Bières bouteilles")
|
||||||
@ -525,6 +527,9 @@ Welcome to the wiki page!
|
|||||||
club=main_club,
|
club=main_club,
|
||||||
)
|
)
|
||||||
cotis.save()
|
cotis.save()
|
||||||
|
cotis.buying_groups.add(subscribers)
|
||||||
|
cotis.buying_groups.add(old_subscribers)
|
||||||
|
cotis.save()
|
||||||
cotis2 = Product(
|
cotis2 = Product(
|
||||||
name="Cotis 2 semestres",
|
name="Cotis 2 semestres",
|
||||||
code="2SCOTIZ",
|
code="2SCOTIZ",
|
||||||
@ -535,6 +540,9 @@ Welcome to the wiki page!
|
|||||||
club=main_club,
|
club=main_club,
|
||||||
)
|
)
|
||||||
cotis2.save()
|
cotis2.save()
|
||||||
|
cotis2.buying_groups.add(subscribers)
|
||||||
|
cotis2.buying_groups.add(old_subscribers)
|
||||||
|
cotis2.save()
|
||||||
refill = Product(
|
refill = Product(
|
||||||
name="Rechargement 15 €",
|
name="Rechargement 15 €",
|
||||||
code="15REFILL",
|
code="15REFILL",
|
||||||
@ -545,6 +553,8 @@ Welcome to the wiki page!
|
|||||||
club=main_club,
|
club=main_club,
|
||||||
)
|
)
|
||||||
refill.save()
|
refill.save()
|
||||||
|
refill.buying_groups.add(subscribers)
|
||||||
|
refill.save()
|
||||||
barb = Product(
|
barb = Product(
|
||||||
name="Barbar",
|
name="Barbar",
|
||||||
code="BARB",
|
code="BARB",
|
||||||
@ -553,8 +563,11 @@ Welcome to the wiki page!
|
|||||||
selling_price="1.7",
|
selling_price="1.7",
|
||||||
special_selling_price="1.6",
|
special_selling_price="1.6",
|
||||||
club=main_club,
|
club=main_club,
|
||||||
|
limit_age=18,
|
||||||
)
|
)
|
||||||
barb.save()
|
barb.save()
|
||||||
|
barb.buying_groups.add(subscribers)
|
||||||
|
barb.save()
|
||||||
cble = Product(
|
cble = Product(
|
||||||
name="Chimay Bleue",
|
name="Chimay Bleue",
|
||||||
code="CBLE",
|
code="CBLE",
|
||||||
@ -563,8 +576,11 @@ Welcome to the wiki page!
|
|||||||
selling_price="1.7",
|
selling_price="1.7",
|
||||||
special_selling_price="1.6",
|
special_selling_price="1.6",
|
||||||
club=main_club,
|
club=main_club,
|
||||||
|
limit_age=18,
|
||||||
)
|
)
|
||||||
cble.save()
|
cble.save()
|
||||||
|
cble.buying_groups.add(subscribers)
|
||||||
|
cble.save()
|
||||||
cons = Product(
|
cons = Product(
|
||||||
name="Consigne Eco-cup",
|
name="Consigne Eco-cup",
|
||||||
code="CONS",
|
code="CONS",
|
||||||
@ -574,7 +590,6 @@ Welcome to the wiki page!
|
|||||||
special_selling_price="1",
|
special_selling_price="1",
|
||||||
club=main_club,
|
club=main_club,
|
||||||
)
|
)
|
||||||
cons.id = 1152
|
|
||||||
cons.save()
|
cons.save()
|
||||||
dcons = Product(
|
dcons = Product(
|
||||||
name="Déconsigne Eco-cup",
|
name="Déconsigne Eco-cup",
|
||||||
@ -585,9 +600,8 @@ Welcome to the wiki page!
|
|||||||
special_selling_price="-1",
|
special_selling_price="-1",
|
||||||
club=main_club,
|
club=main_club,
|
||||||
)
|
)
|
||||||
dcons.id = 1151
|
|
||||||
dcons.save()
|
dcons.save()
|
||||||
Product(
|
cors = Product(
|
||||||
name="Corsendonk",
|
name="Corsendonk",
|
||||||
code="CORS",
|
code="CORS",
|
||||||
product_type=p,
|
product_type=p,
|
||||||
@ -595,8 +609,12 @@ Welcome to the wiki page!
|
|||||||
selling_price="1.7",
|
selling_price="1.7",
|
||||||
special_selling_price="1.6",
|
special_selling_price="1.6",
|
||||||
club=main_club,
|
club=main_club,
|
||||||
).save()
|
limit_age=18,
|
||||||
Product(
|
)
|
||||||
|
cors.save()
|
||||||
|
cors.buying_groups.add(subscribers)
|
||||||
|
cors.save()
|
||||||
|
carolus = Product(
|
||||||
name="Carolus",
|
name="Carolus",
|
||||||
code="CARO",
|
code="CARO",
|
||||||
product_type=p,
|
product_type=p,
|
||||||
@ -604,7 +622,11 @@ Welcome to the wiki page!
|
|||||||
selling_price="1.7",
|
selling_price="1.7",
|
||||||
special_selling_price="1.6",
|
special_selling_price="1.6",
|
||||||
club=main_club,
|
club=main_club,
|
||||||
).save()
|
limit_age=18,
|
||||||
|
)
|
||||||
|
carolus.save()
|
||||||
|
carolus.buying_groups.add(subscribers)
|
||||||
|
carolus.save()
|
||||||
mde = Counter.objects.filter(name="MDE").first()
|
mde = Counter.objects.filter(name="MDE").first()
|
||||||
mde.products.add(barb)
|
mde.products.add(barb)
|
||||||
mde.products.add(cble)
|
mde.products.add(cble)
|
||||||
|
@ -325,6 +325,13 @@ class User(AbstractBaseUser):
|
|||||||
)
|
)
|
||||||
return s.exists()
|
return s.exists()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def account_balance(self):
|
||||||
|
if hasattr(self, "customer"):
|
||||||
|
return self.customer.amount
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
_club_memberships = {}
|
_club_memberships = {}
|
||||||
_group_names = {}
|
_group_names = {}
|
||||||
_group_ids = {}
|
_group_ids = {}
|
||||||
@ -459,6 +466,20 @@ class User(AbstractBaseUser):
|
|||||||
def is_banned_counter(self):
|
def is_banned_counter(self):
|
||||||
return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID)
|
return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def age(self) -> int:
|
||||||
|
"""
|
||||||
|
Return the age this user has the day the method is called.
|
||||||
|
"""
|
||||||
|
today = timezone.now()
|
||||||
|
age = today.year - self.date_of_birth.year
|
||||||
|
# remove a year if this year's birthday is yet to come
|
||||||
|
age -= (today.month, today.day) < (
|
||||||
|
self.date_of_birth.month,
|
||||||
|
self.date_of_birth.day,
|
||||||
|
)
|
||||||
|
return age
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
create = False
|
create = False
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
5
core/static/core/js/alpinejs.min.js
vendored
Normal file
5
core/static/core/js/alpinejs.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -159,9 +159,9 @@ a {
|
|||||||
|
|
||||||
header {
|
header {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 0px auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
box-shadow: $shadow-color 0px 0px 15px;
|
box-shadow: $shadow-color 0 0 15px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
//background-color: $primary-neutral-dark-color;
|
//background-color: $primary-neutral-dark-color;
|
||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
@ -196,7 +196,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#header_connect_links {
|
#header_connect_links {
|
||||||
margin: 0.6em 0.6em 0em auto;
|
margin: 0.6em 0.6em 0 auto;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
form {
|
form {
|
||||||
@ -221,7 +221,7 @@ header {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin: 0em 1em;
|
margin: 0 1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -247,7 +247,7 @@ header {
|
|||||||
#header_search {
|
#header_search {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
margin: 0.8em 0em;
|
margin: 0.8em 0;
|
||||||
input {
|
input {
|
||||||
width: 14ch;
|
width: 14ch;
|
||||||
}
|
}
|
||||||
@ -258,10 +258,10 @@ header {
|
|||||||
flex: initial;
|
flex: initial;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 0em;
|
margin: 0;
|
||||||
div {
|
div {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: 1.2em 0em;
|
padding: 1.2em 0;
|
||||||
&:first-child {
|
&:first-child {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
}
|
}
|
||||||
@ -283,7 +283,7 @@ header {
|
|||||||
background: white;
|
background: white;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
margin: 1.5em 0em 0em -14em;
|
margin: 1.5em 0 0em -14em;
|
||||||
.header_notif_date {
|
.header_notif_date {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@ -291,7 +291,7 @@ header {
|
|||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
margin: 0em;
|
margin: 0;
|
||||||
color: $black-color;
|
color: $black-color;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-dark-color;
|
color: $primary-dark-color;
|
||||||
@ -319,7 +319,7 @@ header {
|
|||||||
|
|
||||||
#popupheader {
|
#popupheader {
|
||||||
width: 88%;
|
width: 88%;
|
||||||
margin: 0px auto;
|
margin: 0 auto;
|
||||||
padding: 0.3em 1%;
|
padding: 0.3em 1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,15 +362,15 @@ header {
|
|||||||
|
|
||||||
#page {
|
#page {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 0em auto;
|
margin: 20px auto 0;
|
||||||
/*---------------------------------NAV---------------------------------*/
|
/*---------------------------------NAV---------------------------------*/
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background-color: $primary-dark-color;
|
background-color: $primary-dark-color;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
border-radius: 6px 6px 0px 0px;
|
border-radius: 6px 6px 0 0;
|
||||||
box-shadow: $shadow-color 0px 0px 15px;
|
box-shadow: $shadow-color 0 0 15px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
@ -385,10 +385,10 @@ header {
|
|||||||
background: $secondary-neutral-color;
|
background: $secondary-neutral-color;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-radius: 6px 0px 0px 0px;
|
border-radius: 6px 0 0 0;
|
||||||
}
|
}
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
border-radius: 0px 6px 0px 0px;
|
border-radius: 0 6px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -411,7 +411,7 @@ header {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
box-shadow: 3px 3px 3px 0px $shadow-color;
|
box-shadow: 3px 3px 3px 0 $shadow-color;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,7 +436,7 @@ header {
|
|||||||
/*--------------------------------CONTENT------------------------------*/
|
/*--------------------------------CONTENT------------------------------*/
|
||||||
#quick_notif {
|
#quick_notif {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0px auto;
|
margin: 0 auto;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
li {
|
li {
|
||||||
@ -447,7 +447,7 @@ header {
|
|||||||
|
|
||||||
#content {
|
#content {
|
||||||
padding: 1em 1%;
|
padding: 1em 1%;
|
||||||
box-shadow: $shadow-color 0px 5px 10px;
|
box-shadow: $shadow-color 0 5px 10px;
|
||||||
background: $white-color;
|
background: $white-color;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@ -492,7 +492,7 @@ header {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
.news_column {
|
.news_column {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
#news_admin {
|
#news_admin {
|
||||||
@ -533,7 +533,7 @@ header {
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
#agenda_title,#birthdays_title {
|
#agenda_title,#birthdays_title {
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
border-radius: 5px 5px 0px 0px;
|
border-radius: 5px 5px 0 0;
|
||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -602,10 +602,10 @@ header {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
border-radius: 7px 0px 0px 7px;
|
border-radius: 7px 0 0 7px;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
margin: 0px auto;
|
margin: 0 auto;
|
||||||
.day {
|
.day {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
@ -690,7 +690,7 @@ header {
|
|||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
background: $secondary-neutral-light-color;
|
background: $secondary-neutral-light-color;
|
||||||
box-shadow: $shadow-color 0px 0px 2px;
|
box-shadow: $shadow-color 0 0 2px;
|
||||||
border-radius: 18px 5px 18px 5px;
|
border-radius: 18px 5px 18px 5px;
|
||||||
h4 {
|
h4 {
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
@ -1023,7 +1023,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 160%;
|
font-size: 160%;
|
||||||
margin-left: 0px;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@ -1053,7 +1053,7 @@ h6 {
|
|||||||
|
|
||||||
p, pre {
|
p, pre {
|
||||||
margin-top: 0.8em;
|
margin-top: 0.8em;
|
||||||
margin-left: 0px;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol, dl {
|
ul, ol, dl {
|
||||||
@ -1111,7 +1111,7 @@ td {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
> ul {
|
> ul {
|
||||||
margin-top: 0px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1464,7 +1464,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search_bar {
|
.search_bar {
|
||||||
margin: 10px 0px;
|
margin: 10px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
height: 20p;
|
height: 20p;
|
||||||
@ -1480,7 +1480,7 @@ textarea {
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
background: $secondary-color;
|
background: $secondary-color;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 10px 10px 0px 0px;
|
border-radius: 10px 10px 0 0;
|
||||||
.title {
|
.title {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@ -1520,7 +1520,7 @@ textarea {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
img {
|
img {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
margin: 0px auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1608,7 +1608,7 @@ footer {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background-color: $primary-neutral-dark-color;
|
background-color: $primary-neutral-dark-color;
|
||||||
box-shadow: $shadow-color 0px 0px 15px;
|
box-shadow: $shadow-color 0 0 15px;
|
||||||
a {
|
a {
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -1624,7 +1624,7 @@ footer {
|
|||||||
/*---------------------------------FORMS-------------------------------*/
|
/*---------------------------------FORMS-------------------------------*/
|
||||||
|
|
||||||
form {
|
form {
|
||||||
margin: 0px auto;
|
margin: 0 auto;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1638,7 +1638,7 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ui-dialog .ui-dialog-buttonpane {
|
.ui-dialog .ui-dialog-buttonpane {
|
||||||
bottom: 0px;
|
bottom: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 97%;
|
width: 97%;
|
||||||
}
|
}
|
||||||
@ -1683,8 +1683,8 @@ label {
|
|||||||
/*-------------------------------MARKDOWN------------------------------*/
|
/*-------------------------------MARKDOWN------------------------------*/
|
||||||
|
|
||||||
.markdown {
|
.markdown {
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
code {
|
code {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
@ -1715,7 +1715,7 @@ label {
|
|||||||
}
|
}
|
||||||
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-top,
|
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-top,
|
||||||
.ui-corner-left {
|
.ui-corner-left {
|
||||||
border-radius: 0px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#club_detail {
|
#club_detail {
|
||||||
@ -1741,13 +1741,13 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
.pedagogy {
|
.pedagogy {
|
||||||
&.star-not-checked {
|
&.star-not-checked {
|
||||||
color : #f7f7f7;
|
color : #f7f7f7;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0;
|
||||||
margin-top: 0px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
&.star-checked {
|
&.star-checked {
|
||||||
color: $pedagogy-orange;
|
color: $pedagogy-orange;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0;
|
||||||
margin-top: 0px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.grade-without-star {
|
&.grade-without-star {
|
||||||
@ -2119,8 +2119,8 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
grid-area: markdown;
|
grid-area: markdown;
|
||||||
|
|
||||||
min-height: 139px;
|
min-height: 139px;
|
||||||
margin-top: 0px;
|
margin-top: 0;
|
||||||
margin-right: 0px;
|
margin-right: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@ -2166,7 +2166,7 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
"report"
|
"report"
|
||||||
"date"
|
"date"
|
||||||
"author";
|
"author";
|
||||||
margin-top: 0px;
|
margin-top: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2181,7 +2181,7 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
|
|
||||||
@media screen and (max-width: $large-devices){
|
@media screen and (max-width: $large-devices){
|
||||||
clip-path: none;
|
clip-path: none;
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
padding-bottom: 7px;
|
padding-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
191
core/static/eboutic/css/eboutic.css
Normal file
191
core/static/eboutic/css/eboutic.css
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
#eboutic {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: flex-start;
|
||||||
|
column-gap: 20px;
|
||||||
|
margin: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic-title {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic h3 {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#basket {
|
||||||
|
width: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#basket h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 765px) {
|
||||||
|
#eboutic {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 10px;
|
||||||
|
row-gap: 20px;
|
||||||
|
}
|
||||||
|
#eboutic-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
#basket {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic #basket .error-message {
|
||||||
|
margin-top: 5px;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: #f5c6cb 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 7px;
|
||||||
|
}
|
||||||
|
#eboutic #basket .error-message p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .item-list {
|
||||||
|
margin-left: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .item-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .item-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .item-price, #eboutic .item-quantity {
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .item-quantity {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .fa-plus, #eboutic .fa-minus {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .item-price {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS du catalogue */
|
||||||
|
|
||||||
|
#eboutic #catalog {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .category-header {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 15px;
|
||||||
|
row-gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 765px) {
|
||||||
|
#eboutic #catalog {
|
||||||
|
row-gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-group {
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-button {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 150px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
row-gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-button:active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-button img, #eboutic .product-button .fa {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-button p {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .catalog-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 30px;
|
||||||
|
margin: 30px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic input {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .catalog-buttons button {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: white;
|
||||||
|
min-width: 60px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
#eboutic .catalog-buttons .validate {
|
||||||
|
background-color: #354a5f;
|
||||||
|
}
|
||||||
|
#eboutic .catalog-buttons .clear {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
#eboutic .catalog-buttons button i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .catalog-buttons button.validate:hover {
|
||||||
|
background-color: #2c3646;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .catalog-buttons button.clear:hover {
|
||||||
|
background-color:hsl(210,5%,30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .catalog-buttons form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
104
core/static/eboutic/js/eboutic.js
Normal file
104
core/static/eboutic/js/eboutic.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_starting_items() {
|
||||||
|
const cookie = getCookie("basket_items")
|
||||||
|
try {
|
||||||
|
// django cookie backend does an utter mess on non-trivial data types
|
||||||
|
// so we must perform a conversion of our own
|
||||||
|
const biscuit = JSON.parse(JSON.parse(cookie.replace(/\\054/g, ',')));
|
||||||
|
if (Array.isArray(biscuit)) {
|
||||||
|
return biscuit;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('basket', () => ({
|
||||||
|
items: get_starting_items(),
|
||||||
|
|
||||||
|
get_total() {
|
||||||
|
let total = 0;
|
||||||
|
for (const item of this.items) {
|
||||||
|
total += item["quantity"] * item["unit_price"];
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
},
|
||||||
|
|
||||||
|
add(item) {
|
||||||
|
item.quantity++;
|
||||||
|
this.edit_cookies()
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(item_id) {
|
||||||
|
const index = this.items.findIndex(e => e.id === item_id);
|
||||||
|
if (index < 0) return;
|
||||||
|
this.items[index].quantity -= 1;
|
||||||
|
if (this.items[index].quantity === 0) {
|
||||||
|
this.items = this.items.filter((e) => e.id !== this.items[index].id);
|
||||||
|
}
|
||||||
|
this.edit_cookies();
|
||||||
|
},
|
||||||
|
|
||||||
|
clear_basket() {
|
||||||
|
this.items = []
|
||||||
|
this.edit_cookies();
|
||||||
|
},
|
||||||
|
|
||||||
|
edit_cookies() {
|
||||||
|
// a cookie survives an hour
|
||||||
|
document.cookie = "basket_items=" + JSON.stringify(this.items) + ";Max-Age=3600";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an item in the basket if it was not already in
|
||||||
|
* @param id : int the id of the product to add
|
||||||
|
* @param name : String the name of the product
|
||||||
|
* @param price : number the unit price of the product
|
||||||
|
*/
|
||||||
|
create_item(id, name, price) {
|
||||||
|
let new_item = {
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
quantity: 0,
|
||||||
|
unit_price: price
|
||||||
|
};
|
||||||
|
this.items.push(new_item);
|
||||||
|
this.add(new_item);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add an item to the basket.
|
||||||
|
* This is called when the user click
|
||||||
|
* on a button in the catalog (left side of the page)
|
||||||
|
* @param id : int the id of the product to add
|
||||||
|
* @param name : String the name of the product
|
||||||
|
* @param price : number the unit price of the product
|
||||||
|
*/
|
||||||
|
add_from_catalog(id, name, price) {
|
||||||
|
const item = this.items.find(e => e.id === id)
|
||||||
|
if (item === undefined) {
|
||||||
|
this.create_item(id, name, price);
|
||||||
|
} else {
|
||||||
|
// the user clicked on an item which is already in the basket
|
||||||
|
this.add(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
@ -9,7 +9,10 @@
|
|||||||
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
|
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
|
||||||
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
|
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
|
||||||
|
{% block jquery_css %}
|
||||||
|
{# Thile file is quite heavy (around 250kb), so declaring it in a block allows easy removal #}
|
||||||
<link rel="stylesheet" href="{{ static('core/js/ui/jquery-ui.min.css') }}">
|
<link rel="stylesheet" href="{{ static('core/js/ui/jquery-ui.min.css') }}">
|
||||||
|
{% endblock %}
|
||||||
<link rel="preload" as="style" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}" onload="this.onload=null;this.rel='stylesheet'">
|
<link rel="preload" as="style" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}" onload="this.onload=null;this.rel='stylesheet'">
|
||||||
<noscript><link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}"></noscript>
|
<noscript><link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}"></noscript>
|
||||||
<script defer href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
|
<script defer href="{{ static('core/font-awesome/js/fontawesone.min.js') }}"></script>
|
||||||
@ -18,6 +21,8 @@
|
|||||||
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
|
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
|
||||||
<!-- Put here to always have acces to those functions on django widgets -->
|
<!-- Put here to always have acces to those functions on django widgets -->
|
||||||
<script src="{{ static('core/js/script.js') }}"></script>
|
<script src="{{ static('core/js/script.js') }}"></script>
|
||||||
|
{% block additional_css %}{% endblock %}
|
||||||
|
{% block additional_js %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -179,7 +184,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
|
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
|
||||||
<a href="{{ url('sas:main') }}">{% trans %}Gallery{% endtrans %}</a>
|
<a href="{{ url('sas:main') }}">{% trans %}Gallery{% endtrans %}</a>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
<a href="{{ url('eboutic:main') }}">{% trans %}Eboutic{% endtrans %}</a>
|
<a href="{{ url('eboutic:main') }}">{% trans %}Eboutic{% endtrans %}</a>
|
||||||
|
{% endif %}
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="dropbtn">{% trans %}Services{% endtrans %}
|
<button class="dropbtn">{% trans %}Services{% endtrans %}
|
||||||
<i class="fa fa-caret-down"></i>
|
<i class="fa fa-caret-down"></i>
|
||||||
|
@ -31,7 +31,6 @@ from django.urls import reverse
|
|||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
|
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
import random
|
import random
|
||||||
@ -73,7 +72,16 @@ class Customer(models.Model):
|
|||||||
return self.recorded_products - number >= -settings.SITH_ECOCUP_LIMIT
|
return self.recorded_products - number >= -settings.SITH_ECOCUP_LIMIT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_buy(self):
|
def can_buy(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if whether this customer has the right to
|
||||||
|
purchase any item.
|
||||||
|
This must be not confused with the Product.can_be_sold_to(user)
|
||||||
|
method as the present method returns an information
|
||||||
|
about a customer whereas the other tells something
|
||||||
|
about the relation between a User (not a Customer,
|
||||||
|
don't mix them) and a Product.
|
||||||
|
"""
|
||||||
return self.user.subscriptions.last() and (
|
return self.user.subscriptions.last() and (
|
||||||
date.today()
|
date.today()
|
||||||
- self.user.subscriptions.order_by("subscription_end")
|
- self.user.subscriptions.order_by("subscription_end")
|
||||||
@ -81,6 +89,7 @@ class Customer(models.Model):
|
|||||||
.subscription_end
|
.subscription_end
|
||||||
) < timedelta(days=90)
|
) < timedelta(days=90)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def generate_account_id(number):
|
def generate_account_id(number):
|
||||||
number = str(number)
|
number = str(number)
|
||||||
letter = random.choice(string.ascii_lowercase)
|
letter = random.choice(string.ascii_lowercase)
|
||||||
@ -203,12 +212,32 @@ class Product(models.Model):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s (%s)" % (self.name, self.code)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("counter:product_list")
|
return reverse("counter:product_list")
|
||||||
|
|
||||||
|
def can_be_sold_to(self, user: User) -> bool:
|
||||||
|
"""
|
||||||
|
Check if whether the user given in parameter has the right to buy
|
||||||
|
this product or not.
|
||||||
|
|
||||||
|
This must be not confused with the Customer.can_buy()
|
||||||
|
method as the present method returns an information
|
||||||
|
about the relation between a User and a Product,
|
||||||
|
whereas the other tells something about a Customer
|
||||||
|
(and not a user, they are not the same model).
|
||||||
|
|
||||||
|
:return: True if the user can buy this product else False
|
||||||
|
"""
|
||||||
|
if not self.buying_groups.exists():
|
||||||
|
return True
|
||||||
|
for group in self.buying_groups.all():
|
||||||
|
if user.is_in_group(group.name):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s (%s)" % (self.name, self.code)
|
||||||
|
|
||||||
|
|
||||||
class Counter(models.Model):
|
class Counter(models.Model):
|
||||||
name = models.CharField(_("name"), max_length=30)
|
name = models.CharField(_("name"), max_length=30)
|
||||||
|
94
eboutic/README.md
Normal file
94
eboutic/README.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
Eboutic
|
||||||
|
=======
|
||||||
|
|
||||||
|
# Aperçu général
|
||||||
|
|
||||||
|
L'application eboutic contient les vues, les templates et les modèles
|
||||||
|
nécessaires pour le fonctionnement de la boutique en ligne.
|
||||||
|
L'utilisateur peut acheter des produits en ligne et payer soit
|
||||||
|
par carte bancaire soit avec l'argent qu'il possède sur son compte AE.
|
||||||
|
|
||||||
|
On effectue ici une séparation entre plusieurs types de produits :
|
||||||
|
- les recharges de compte (``Refilling``) qui permettent de rajouter de
|
||||||
|
l'argent sur le compte. Un panier contenant un élément de ce type ne pourra être
|
||||||
|
payé que par carte bancaire.
|
||||||
|
- Les cotisations (``Subscription``) qui permettent à un utilisateur de cotiser
|
||||||
|
à l'AE. Seul un utilisateur ayant déjà été cotisant pourra acheter ce produit.
|
||||||
|
Les primo-cotisants doivent s'adresser directement à un membre du bureau.
|
||||||
|
- Les produits normaux (badges, écussons, billets pour les soirées...)
|
||||||
|
|
||||||
|
Les utilisateurs cotisants ne peuvent pas voir les produits des non-cotisants
|
||||||
|
et vice-versa. Ce n'est pas la manière la plus satisfaisante de procéder, mais
|
||||||
|
c'est celle qui touchait le moins aux tables déjà en place.
|
||||||
|
Les produits avec une limite d'âge ne seront pas visibles par les utilisateurs ayant
|
||||||
|
un âge inférieur.
|
||||||
|
|
||||||
|
Le comportement de cette application dépend directement de celui des applications
|
||||||
|
``core`` et ``counter``. Les modèles de l'application ``Subscription`` sont
|
||||||
|
également utilisés. N'hésitez donc pas à vous pencher sur le fonctionnement des modules
|
||||||
|
susnommés afin de comprendre comment celui-ci marche.
|
||||||
|
|
||||||
|
# Les vues
|
||||||
|
|
||||||
|
Cette application contient les vues suivantes :
|
||||||
|
|
||||||
|
- `eboutic_main` (GET) : la vue retournant la page principale de la boutique en ligne.
|
||||||
|
Cette vue effectue un filtrage des produits à montrer à l'utilisateur en
|
||||||
|
fonction de ce qu'il a le droit d'acheter.
|
||||||
|
Si cette vue est appelée lors d'une redirection parce qu'une erreur
|
||||||
|
est survenue au cours de la navigation sur la boutique, il est possible
|
||||||
|
de donner les messages d'erreur à donner à l'utilisateur dans la session
|
||||||
|
avec la clef ``"errors"``.
|
||||||
|
- ``payment_result`` (GET) : retourne une page assez simple disant à l'utilisateur
|
||||||
|
si son paiement a échoué ou réussi. Cette vue est appelée par redirection
|
||||||
|
lorsque l'utilisateur paye son panier avec son argent du compte AE.
|
||||||
|
- ``EbouticCommand`` (POST) : traite la soumission d'un panier par l'utilisateur.
|
||||||
|
Lors de l'appel de cette vue, la requête doit contenir un cookie avec l'état
|
||||||
|
du panier à valider. Ce panier doit strictement être de la forme :
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{"id": <int>, "name": <str>, "quantity": <int>, "unit_price": <float>},
|
||||||
|
{"id": <int>, "name": <str>, "quantity": <int>, "unit_price": <float>},
|
||||||
|
<etc.>
|
||||||
|
]
|
||||||
|
```
|
||||||
|
Si le panier est mal formaté ou contient des valeurs invalides,
|
||||||
|
une redirection est faite vers `eboutic_main`.
|
||||||
|
- ``pay_with_sith`` (POST) : paie le panier avec l'argent présent sur le compte
|
||||||
|
AE. Redirige vers `payment_result`.
|
||||||
|
- ``ETransactionAutoAnswer`` (GET) : vue destinée à communiquer avec le service
|
||||||
|
de paiement bancaire pour valider ou non le paiement de l'utilisateur.
|
||||||
|
|
||||||
|
# Les templates
|
||||||
|
|
||||||
|
- ``eboutic_payment_result.jinja`` : très court template contenant juste
|
||||||
|
un message pour dire à l'utilisateur si son achat s'est bien déroulé.
|
||||||
|
Retourné par la vue ``payment_result``.
|
||||||
|
- ``eboutic_makecommand.jinja`` : template contenant un résumé du panier et deux
|
||||||
|
boutons, un pour payer avec le site AE et l'autre pour payer par carte bancaire.
|
||||||
|
Retourné par la vue ``EbouticCommand``
|
||||||
|
- ``eboutic_main.jinja`` : le plus gros template de cette application. Contient
|
||||||
|
une interface pour que l'utilisateur puisse consulter les produits et remplir
|
||||||
|
son panier. Les opérations de remplissage du panier se font entièrement côté client.
|
||||||
|
À chaque clic pour ajouter ou retirer un élément du panier, le script JS
|
||||||
|
(AlpineJS, plus précisément) édite en même temps un cookie.
|
||||||
|
Au moment de la validation du panier, ce cookie est envoyé au serveur pour
|
||||||
|
vérifier que la commande est valide et payer.
|
||||||
|
|
||||||
|
# Les modèles
|
||||||
|
|
||||||
|
- ``Basket`` : représente le panier d'un utilisateur. Un objet ``Basket`` est créé
|
||||||
|
quand l'utilisateur soumet son panier et supprimé quand le paiement est validé.
|
||||||
|
Si le paiement n'est pas validé, le panier est conservé en base de données
|
||||||
|
afin d'avoir une trace si l'utilisateur a quand même été débité et qu'il n'a pas
|
||||||
|
reçu ses produits à cause d'une erreur de synchronisation.
|
||||||
|
- ``Invoice`` : Un peu comme un panier, mais pour quand une transaction
|
||||||
|
a été validée.
|
||||||
|
- ``AbstractBaseItem`` : modèle utilisé pour être hérité par ``BasketItem`` et ``InvoiceItem``.
|
||||||
|
Me demandez pas pourquoi ``product_id`` est un IntegerField plutôt qu'une clef étrangère,
|
||||||
|
moi aussi je trouve ça complètement con.
|
||||||
|
Le prix unitaire est indiqué dans ce modèle même si ça fait une redondance avec le modèle
|
||||||
|
``Product`` afin de garder le prix que l'élément avait au moment de sa vente,
|
||||||
|
même si le prix du produit de base est modifié.
|
||||||
|
- ``BasketItem`` : représente un élément présent dans le panier de l'utilisateur.
|
||||||
|
- ``InvoiceItem`` : représente un élément vendu.
|
@ -28,3 +28,4 @@ from eboutic.models import *
|
|||||||
|
|
||||||
admin.site.register(Basket)
|
admin.site.register(Basket)
|
||||||
admin.site.register(Invoice)
|
admin.site.register(Invoice)
|
||||||
|
admin.site.register(BasketItem)
|
||||||
|
39
eboutic/converters.py
Normal file
39
eboutic/converters.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2022
|
||||||
|
# - Maréchal <thgirod@hotmail.com
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentResultConverter:
|
||||||
|
"""
|
||||||
|
Converter used for url mapping of the ``eboutic.views.payment_result``
|
||||||
|
view.
|
||||||
|
It's meant to build an url that can match
|
||||||
|
either ``/eboutic/pay/success/`` or ``/eboutic/pay/failure/``
|
||||||
|
but nothing else.
|
||||||
|
"""
|
||||||
|
|
||||||
|
regex = "(success|failure)"
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def to_url(self, value):
|
||||||
|
return str(value)
|
175
eboutic/forms.py
Normal file
175
eboutic/forms.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2022
|
||||||
|
# - Maréchal <thgirod@hotmail.com
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from eboutic.models import get_eboutic_products
|
||||||
|
|
||||||
|
|
||||||
|
class BasketForm:
|
||||||
|
"""
|
||||||
|
Class intended to perform checks on the request sended to the server when
|
||||||
|
the user submits his basket from /eboutic/
|
||||||
|
|
||||||
|
Because it must check an unknown number of fields, coming from a cookie
|
||||||
|
and needing some databases checks to be performed, inheriting from forms.Form
|
||||||
|
or using formset would have been likely to end in a big ball of wibbly-wobbly hacky stuff.
|
||||||
|
Thus this class is a pure standalone and performs its operations by its own means.
|
||||||
|
However, it still tries to share some similarities with a standard django Form.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
::
|
||||||
|
|
||||||
|
def my_view(request):
|
||||||
|
form = BasketForm(request)
|
||||||
|
form.clean()
|
||||||
|
if form.is_valid():
|
||||||
|
# perform operations
|
||||||
|
else:
|
||||||
|
errors = form.get_error_messages()
|
||||||
|
|
||||||
|
# return the cookie that was in the request, but with all
|
||||||
|
# incorrects elements removed
|
||||||
|
cookie = form.get_cleaned_cookie()
|
||||||
|
|
||||||
|
You can also use a little shortcut by directly calling `form.is_valid()`
|
||||||
|
without calling `form.clean()`. In this case, the latter method shall be
|
||||||
|
implicitly called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# check the json is an array containing non-nested objects.
|
||||||
|
# values must be strings or numbers
|
||||||
|
# this is matched :
|
||||||
|
# [{"id": 4, "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||||
|
# but this is not :
|
||||||
|
# [{"id": {"nested_id": 10}, "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||||
|
# and neither does this :
|
||||||
|
# [{"id": ["nested_id": 10], "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||||
|
# and neither does that :
|
||||||
|
# [{"id": null, "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||||
|
json_cookie_re = re.compile(
|
||||||
|
r"^\[\s*(\{\s*(\"[^\"]*\":\s*(\"[^\"]{0,64}\"|\d{0,5}\.?\d+),?\s*)*\},?\s*)*\s*\]$"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, request: HttpRequest):
|
||||||
|
self.user = request.user
|
||||||
|
self.cookies = request.COOKIES
|
||||||
|
self.error_messages = set()
|
||||||
|
self.correct_cookie = []
|
||||||
|
|
||||||
|
def clean(self) -> None:
|
||||||
|
"""
|
||||||
|
Perform all the checks, but return nothing.
|
||||||
|
To know if the form is valid, the `is_valid()` method must be used.
|
||||||
|
|
||||||
|
The form shall be considered as valid if it meets all the following conditions :
|
||||||
|
- it contains a "basket_items" key in the cookies of the request given in the constructor
|
||||||
|
- this cookie is a list of objects formatted this way : `[{'id': <int>, 'quantity': <int>,
|
||||||
|
'name': <str>, 'unit_price': <float>}, ...]`. The order of the fields in each object does not matter
|
||||||
|
- all the ids are positive integers
|
||||||
|
- all the ids refer to products available in the EBOUTIC
|
||||||
|
- all the ids refer to products the user is allowed to buy
|
||||||
|
- all the quantities are positive integers
|
||||||
|
"""
|
||||||
|
basket = self.cookies.get("basket_items", None)
|
||||||
|
if basket is None or basket in ("[]", ""):
|
||||||
|
self.error_messages.add(_("You have no basket."))
|
||||||
|
return
|
||||||
|
# check that the json is not nested before parsing it to make sure
|
||||||
|
# malicious user can't ddos the server with deeply nested json
|
||||||
|
if not BasketForm.json_cookie_re.match(basket):
|
||||||
|
self.error_messages.add(_("The request was badly formatted."))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
basket = json.loads(basket)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self.error_messages.add(_("The basket cookie was badly formatted."))
|
||||||
|
return
|
||||||
|
if type(basket) is not list or len(basket) == 0:
|
||||||
|
self.error_messages.add(_("Your basket is empty."))
|
||||||
|
return
|
||||||
|
for item in basket:
|
||||||
|
expected_keys = {"id", "quantity", "name", "unit_price"}
|
||||||
|
if type(item) is not dict or set(item.keys()) != expected_keys:
|
||||||
|
self.error_messages.add("One or more items are badly formatted.")
|
||||||
|
continue
|
||||||
|
# check the id field is a positive integer
|
||||||
|
if type(item["id"]) is not int or item["id"] < 0:
|
||||||
|
self.error_messages.add(
|
||||||
|
_("%(name)s : this product does not exist.")
|
||||||
|
% {"name": item["name"]}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# check a product with this id does exist
|
||||||
|
ids = {product.id for product in get_eboutic_products(self.user)}
|
||||||
|
if not item["id"] in ids:
|
||||||
|
self.error_messages.add(
|
||||||
|
_(
|
||||||
|
"%(name)s : this product does not exist or may no longer be available."
|
||||||
|
)
|
||||||
|
% {"name": item["name"]}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if type(item["quantity"]) is not int or item["quantity"] < 0:
|
||||||
|
self.error_messages.add(
|
||||||
|
_("You cannot buy %(nbr)d %(name)%s.")
|
||||||
|
% {"nbr": item["quantity"], "name": item["name"]}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if we arrive here, it means this item has passed all tests
|
||||||
|
self.correct_cookie.append(item)
|
||||||
|
# for loop for item checking ends here
|
||||||
|
|
||||||
|
# this function does not return anything.
|
||||||
|
# instead, it fills a set containing the collected error messages
|
||||||
|
# an empty set means that no error was seen thus everything is ok
|
||||||
|
# and the form is valid.
|
||||||
|
# a non-empty set means there was at least one error thus
|
||||||
|
# the form is invalid
|
||||||
|
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
"""
|
||||||
|
return True if the form is correct else False.
|
||||||
|
If the `clean()` method has not been called beforehand, call it
|
||||||
|
"""
|
||||||
|
if self.error_messages == set() and self.correct_cookie == []:
|
||||||
|
self.clean()
|
||||||
|
if self.error_messages:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_error_messages(self) -> typing.List[str]:
|
||||||
|
# return [msg for msg in self.error_messages]
|
||||||
|
return list(self.error_messages)
|
||||||
|
|
||||||
|
def get_cleaned_cookie(self) -> str:
|
||||||
|
if not self.correct_cookie:
|
||||||
|
return ""
|
||||||
|
return json.dumps(self.correct_cookie)
|
23
eboutic/migrations/0002_auto_20221005_2243.py
Normal file
23
eboutic/migrations/0002_auto_20221005_2243.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.15 on 2022-10-05 20:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("eboutic", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="basketitem",
|
||||||
|
name="quantity",
|
||||||
|
field=models.PositiveIntegerField(verbose_name="quantity"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="invoiceitem",
|
||||||
|
name="quantity",
|
||||||
|
field=models.PositiveIntegerField(verbose_name="quantity"),
|
||||||
|
),
|
||||||
|
]
|
@ -21,15 +21,29 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
import typing
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from django.db import models, DataError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import models, DataError
|
||||||
|
from django.db.models import Sum, F
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounting.models import CurrencyField
|
from accounting.models import CurrencyField
|
||||||
|
from core.models import Group, User
|
||||||
from counter.models import Counter, Product, Selling, Refilling
|
from counter.models import Counter, Product, Selling, Refilling
|
||||||
from core.models import User
|
|
||||||
|
|
||||||
|
def get_eboutic_products(user: User) -> List[Product]:
|
||||||
|
products = (
|
||||||
|
Counter.objects.get(type="EBOUTIC")
|
||||||
|
.products.filter(product_type__isnull=False)
|
||||||
|
.filter(archived=False)
|
||||||
|
.filter(limit_age__lte=user.age)
|
||||||
|
.annotate(category=F("product_type__name"))
|
||||||
|
)
|
||||||
|
return [p for p in products if p.can_be_sold_to(user)]
|
||||||
|
|
||||||
|
|
||||||
class Basket(models.Model):
|
class Basket(models.Model):
|
||||||
@ -46,7 +60,13 @@ class Basket(models.Model):
|
|||||||
)
|
)
|
||||||
date = models.DateTimeField(_("date"), auto_now=True)
|
date = models.DateTimeField(_("date"), auto_now=True)
|
||||||
|
|
||||||
def add_product(self, p, q=1):
|
def add_product(self, p: Product, q: int = 1):
|
||||||
|
"""
|
||||||
|
Given p an object of the Product model and q an integer,
|
||||||
|
add q items corresponding to this Product from the basket.
|
||||||
|
|
||||||
|
If this function is called with a product not in the basket, no error will be raised
|
||||||
|
"""
|
||||||
item = self.items.filter(product_id=p.id).first()
|
item = self.items.filter(product_id=p.id).first()
|
||||||
if item is None:
|
if item is None:
|
||||||
BasketItem(
|
BasketItem(
|
||||||
@ -61,25 +81,53 @@ class Basket(models.Model):
|
|||||||
item.quantity += q
|
item.quantity += q
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
def del_product(self, p, q=1):
|
def del_product(self, p: Product, q: int = 1):
|
||||||
item = self.items.filter(product_id=p.id).first()
|
"""
|
||||||
if item is not None:
|
Given p an object of the Product model and q an integer,
|
||||||
|
remove q items corresponding to this Product from the basket.
|
||||||
|
|
||||||
|
If this function is called with a product not in the basket, no error will be raised
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
item = self.items.get(product_id=p.id)
|
||||||
|
except BasketItem.DoesNotExist:
|
||||||
|
return
|
||||||
item.quantity -= q
|
item.quantity -= q
|
||||||
item.save()
|
|
||||||
if item.quantity <= 0:
|
if item.quantity <= 0:
|
||||||
item.delete()
|
item.delete()
|
||||||
|
else:
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""
|
||||||
|
Remove all items from this basket without deleting the basket
|
||||||
|
"""
|
||||||
|
BasketItem.objects.filter(basket=self).delete()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def contains_refilling_item(self):
|
def contains_refilling_item(self) -> bool:
|
||||||
return self.items.filter(
|
return self.items.filter(
|
||||||
type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
def get_total(self):
|
def get_total(self) -> float:
|
||||||
total = 0
|
total = self.items.aggregate(
|
||||||
for i in self.items.all():
|
total=Sum(F("quantity") * F("product_unit_price"))
|
||||||
total += i.quantity * i.product_unit_price
|
)["total"]
|
||||||
return total
|
return float(total) if total is not None else 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_session(cls, session) -> typing.Union["Basket", None]:
|
||||||
|
"""
|
||||||
|
Given an HttpRequest django object, return the basket used in the current session
|
||||||
|
if it exists else create a new one and return it
|
||||||
|
"""
|
||||||
|
if "basket_id" in session:
|
||||||
|
try:
|
||||||
|
return cls.objects.get(id=session["basket_id"])
|
||||||
|
except cls.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s's basket (%d items)" % (self.user, self.items.all().count())
|
return "%s's basket (%d items)" % (self.user, self.items.all().count())
|
||||||
@ -100,14 +148,11 @@ class Invoice(models.Model):
|
|||||||
date = models.DateTimeField(_("date"), auto_now=True)
|
date = models.DateTimeField(_("date"), auto_now=True)
|
||||||
validated = models.BooleanField(_("validated"), default=False)
|
validated = models.BooleanField(_("validated"), default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def get_total(self) -> float:
|
||||||
return "%s - %s - %s" % (self.user, self.get_total(), self.date)
|
total = self.items.aggregate(
|
||||||
|
total=Sum(F("quantity") * F("product_unit_price"))
|
||||||
def get_total(self):
|
)["total"]
|
||||||
total = 0
|
return float(total) if total is not None else 0
|
||||||
for i in self.items.all():
|
|
||||||
total += i.quantity * i.product_unit_price
|
|
||||||
return total
|
|
||||||
|
|
||||||
def validate(self, *args, **kwargs):
|
def validate(self, *args, **kwargs):
|
||||||
if self.validated:
|
if self.validated:
|
||||||
@ -153,13 +198,16 @@ class Invoice(models.Model):
|
|||||||
self.validated = True
|
self.validated = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s - %s - %s" % (self.user, self.get_total(), self.date)
|
||||||
|
|
||||||
|
|
||||||
class AbstractBaseItem(models.Model):
|
class AbstractBaseItem(models.Model):
|
||||||
product_id = models.IntegerField(_("product id"))
|
product_id = models.IntegerField(_("product id"))
|
||||||
product_name = models.CharField(_("product name"), max_length=255)
|
product_name = models.CharField(_("product name"), max_length=255)
|
||||||
type_id = models.IntegerField(_("product type id"))
|
type_id = models.IntegerField(_("product type id"))
|
||||||
product_unit_price = CurrencyField(_("unit price"))
|
product_unit_price = CurrencyField(_("unit price"))
|
||||||
quantity = models.IntegerField(_("quantity"))
|
quantity = models.PositiveIntegerField(_("quantity"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
@ -1,80 +1,110 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans %}Eboutic{% endtrans %}
|
{% trans %}Eboutic{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% macro add_product(id, content, class="") %}
|
{% block jquery_css %}
|
||||||
<form method="post" action="{{ url('eboutic:main') }}" class="inline {{ class }}">
|
{# Remove jquery css #}
|
||||||
{% csrf_token %}
|
{% endblock %}
|
||||||
<input type="hidden" name="action" value="add_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro del_product(id, content) %}
|
{% block additional_js %}
|
||||||
<form method="post" action="{{ url('eboutic:main') }}" class="inline" style="display:inline">
|
{# This script contains the code to perform requests to manipulate the
|
||||||
{% csrf_token %}
|
user basket without having to reload the page #}
|
||||||
<input type="hidden" name="action" value="del_product">
|
<script src="{{ static('eboutic/js/eboutic.js') }}"></script>
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
|
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
|
||||||
</form>
|
{% endblock %}
|
||||||
{% endmacro %}
|
|
||||||
|
{% block additional_css %}
|
||||||
|
<link rel="stylesheet" href="{{ static('eboutic/css/eboutic.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans %}Eboutic{% endtrans %}</h3>
|
<h1 id="eboutic-title">{% trans %}Eboutic{% endtrans %}</h1>
|
||||||
|
<div id="eboutic" x-data="basket">
|
||||||
<div id="basket">
|
<div id="basket">
|
||||||
<p>{% trans %}Basket: {% endtrans %}</p>
|
<h3>Panier</h3>
|
||||||
<ul>
|
{% if errors %}
|
||||||
{% for i in basket.items.all()|sort(attribute='id') %}
|
<div class="error-message">
|
||||||
<li>{{ del_product(i.product_id, '-') }} {{ i.quantity }}
|
{% for error in errors %}
|
||||||
{{ add_product(i.product_id, '+') }} {{ i.product_name }}: {{ "%0.2f"|format(i.product_unit_price*i.quantity) }} €</li>
|
<p>{{ error }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% trans %}Your basket has been cleaned accordingly to those errors.{% endtrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<ul class="item-list">
|
||||||
|
{# Starting money #}
|
||||||
|
<li>
|
||||||
|
<span class="item-name">
|
||||||
|
<strong>{% trans %}Current account amount: {% endtrans %}</strong>
|
||||||
|
</span>
|
||||||
|
<span class="item-price">
|
||||||
|
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<template x-for="item in items" :key="item.id">
|
||||||
|
<li class="item-row" x-show="item.quantity > 0">
|
||||||
|
<span class="item-name" x-text="item.name"></span>
|
||||||
|
<div class="item-quantity">
|
||||||
|
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
||||||
|
<span x-text="item.quantity"></span>
|
||||||
|
<i class="fa fa-plus" @click="add(item)"></i>
|
||||||
|
</div>
|
||||||
|
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
{# Total price #}
|
||||||
|
<li style="margin-top: 20px">
|
||||||
|
<span class="item-name"><strong>{% trans %}Basket amount: {% endtrans %}</strong></span>
|
||||||
|
<span x-text="get_total().toFixed(2) + ' €'" class="item-price"></span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<div class="catalog-buttons">
|
||||||
<strong>{% trans %}Basket amount: {% endtrans %}{{ "%0.2f"|format(basket.get_total()) }} €</strong>
|
<button @click="clear_basket()" class="clear">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
{% if customer_amount != None %}
|
{% trans %}Clear{% endtrans %}
|
||||||
<br>
|
</button>
|
||||||
{% trans %}Current account amount: {% endtrans %}<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
|
||||||
|
|
||||||
{% if not basket.contains_refilling_item %}
|
|
||||||
<br>
|
|
||||||
{% trans %}Remaining account amount: {% endtrans %}<strong>{{ "%0.2f"|format(customer_amount - basket.get_total()) }} €</strong>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="post" action="{{ url('eboutic:command') }}">
|
<form method="post" action="{{ url('eboutic:command') }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<p>
|
<button class="validate">
|
||||||
<input type="submit" value="{% trans %}Proceed to command{% endtrans %}" />
|
<i class="fa fa-check"></i>
|
||||||
</p>
|
<input type="submit" value="{% trans %}Validate{% endtrans %}"/>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
{% for t in categories %}
|
<div id="catalog">
|
||||||
{% if eboutic.products.filter(product_type=t).exists() %}
|
{% for category, items in products|groupby('category') %}
|
||||||
<h5>{{ t }}</h5>
|
{% if items|count > 0 %}
|
||||||
{% if t.comment %}
|
<section>
|
||||||
<p>{{ t.comment }}</p>
|
{# I would have wholeheartedly directly used the header element instead
|
||||||
|
but it has already been made messy in core/style.scss #}
|
||||||
|
<div class="category-header">
|
||||||
|
<h3>{{ category }}</h3>
|
||||||
|
{% if category.comment %}
|
||||||
|
<p>{{ category.comment }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br />
|
</div>
|
||||||
{% for p in eboutic.products.filter(product_type=t).all() %}
|
<div class="product-group">
|
||||||
{% set file = None %}
|
{% for p in items %}
|
||||||
|
<button class="product-button"
|
||||||
|
@click="add_from_catalog({{ p.id }}, '{{ p.name }}', {{ p.selling_price }})">
|
||||||
{% if p.icon %}
|
{% if p.icon %}
|
||||||
{% set file = p.icon.url %}
|
<img src="{{ p.icon.url }}" alt="image de {{ p.product_name }}" width="40px"
|
||||||
|
height="40px">
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set file = static('core/img/na.gif') %}
|
<i class="fa fa-2x fa-picture-o"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €</span>' % (p.name, file, p.selling_price) %}
|
<p><strong>{{ p.name }}</strong></p>
|
||||||
{{ add_product(p.id, prod, "form_button") }}
|
<p>{{ p.selling_price }} €</p>
|
||||||
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans %}There are no items available for sale{% endtrans %}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@
|
|||||||
|
|
||||||
{% if not basket.contains_refilling_item %}
|
{% if not basket.contains_refilling_item %}
|
||||||
<br>
|
<br>
|
||||||
{% trans %}Remaining account amount: {% endtrans %}<strong>{{ "%0.2f"|format(customer_amount - basket.get_total()) }} €</strong>
|
{% trans %}Remaining account amount: {% endtrans %}
|
||||||
|
<strong>{{ "%0.2f"|format(customer_amount|float - basket.get_total()) }} €</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
@ -61,7 +62,6 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,14 +4,16 @@
|
|||||||
<h3>{% trans %}Eboutic{% endtrans %}</h3>
|
<h3>{% trans %}Eboutic{% endtrans %}</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{% if not_enough %}
|
{% if success %}
|
||||||
{% trans %}Payment failed{% endtrans %}
|
|
||||||
{% else %}
|
|
||||||
{% trans %}Payment successful{% endtrans %}
|
{% trans %}Payment successful{% endtrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans %}Payment failed{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><a href="{{ url('eboutic:main') }}">{% trans %}Return to eboutic{% endtrans %}</a></p>
|
<p><a href="{{ url('eboutic:main') }}">{% trans %}Return to eboutic{% endtrans %}</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
364
eboutic/tests.py
364
eboutic/tests.py
@ -2,6 +2,7 @@
|
|||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
|
# - Maréchal <thgirod@hotmail.com
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
@ -21,20 +22,21 @@
|
|||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import re
|
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
import re
|
||||||
import urllib
|
import urllib
|
||||||
from OpenSSL import crypto
|
|
||||||
|
|
||||||
|
from OpenSSL import crypto
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.db.models import Max
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.management import call_command
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from counter.models import Product, Counter, Refilling
|
from counter.models import Product, Counter, Customer, Selling
|
||||||
|
from eboutic.models import Basket
|
||||||
|
|
||||||
|
|
||||||
class EbouticTest(TestCase):
|
class EbouticTest(TestCase):
|
||||||
@ -49,6 +51,19 @@ class EbouticTest(TestCase):
|
|||||||
self.cotis = Product.objects.filter(code="1SCOTIZ").first()
|
self.cotis = Product.objects.filter(code="1SCOTIZ").first()
|
||||||
self.eboutic = Counter.objects.filter(name="Eboutic").first()
|
self.eboutic = Counter.objects.filter(name="Eboutic").first()
|
||||||
|
|
||||||
|
def get_busy_basket(self, user):
|
||||||
|
"""
|
||||||
|
Create and return a basket with 3 barbar and 1 cotis in it.
|
||||||
|
Edit the client session to store the basket id in it
|
||||||
|
"""
|
||||||
|
session = self.client.session
|
||||||
|
basket = Basket.objects.create(user=user)
|
||||||
|
session["basket_id"] = basket.id
|
||||||
|
session.save()
|
||||||
|
basket.add_product(self.barbar, 3)
|
||||||
|
basket.add_product(self.cotis)
|
||||||
|
return basket
|
||||||
|
|
||||||
def generate_bank_valid_answer_from_page_content(self, content):
|
def generate_bank_valid_answer_from_page_content(self, content):
|
||||||
content = str(content)
|
content = str(content)
|
||||||
basket_id = re.search(r"PBX_CMD\" value=\"(\d*)\"", content).group(1)
|
basket_id = re.search(r"PBX_CMD\" value=\"(\d*)\"", content).group(1)
|
||||||
@ -69,177 +84,99 @@ class EbouticTest(TestCase):
|
|||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def test_buy_simple_product_with_sith_account(self):
|
def test_buy_with_sith_account(self):
|
||||||
self.client.login(username="subscriber", password="plop")
|
self.client.login(username="subscriber", password="plop")
|
||||||
Refilling(
|
self.subscriber.customer.amount = 100 # give money before test
|
||||||
amount=10,
|
self.subscriber.customer.save()
|
||||||
counter=self.eboutic,
|
basket = self.get_busy_basket(self.subscriber)
|
||||||
operator=self.skia,
|
amount = basket.get_total()
|
||||||
customer=self.subscriber.customer,
|
response = self.client.post(reverse("eboutic:pay_with_sith"))
|
||||||
).save()
|
self.assertRedirects(response, "/eboutic/pay/success/")
|
||||||
response = self.client.post(
|
new_balance = Customer.objects.get(user=self.subscriber).amount
|
||||||
reverse("eboutic:main"),
|
self.assertEqual(float(new_balance), 100 - amount)
|
||||||
{"action": "add_product", "product_id": self.barbar.id},
|
self.assertEqual(
|
||||||
)
|
'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic',
|
||||||
self.assertTrue(
|
self.client.cookies["basket_items"].OutputString(),
|
||||||
'<input type="hidden" name="action" value="add_product">\\n'
|
|
||||||
' <button type="submit" name="product_id" value="4"> + </button>\\n'
|
|
||||||
"</form>\\n Barbar: 1.70 \\xe2\\x82\\xac</li>" in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.post(reverse("eboutic:command"))
|
|
||||||
self.assertTrue(
|
|
||||||
"<tr>\\n <td>Barbar</td>\\n <td>1</td>\\n"
|
|
||||||
" <td>1.70 \\xe2\\x82\\xac</td>\\n </tr>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("eboutic:pay_with_sith"), {"action": "pay_with_sith_account"}
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
"Le paiement a \\xc3\\xa9t\\xc3\\xa9 effectu\\xc3\\xa9\\n"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse(
|
|
||||||
"core:user_account_detail",
|
|
||||||
kwargs={
|
|
||||||
"user_id": self.subscriber.id,
|
|
||||||
"year": datetime.now().year,
|
|
||||||
"month": datetime.now().month,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
'class="selected_tab">Compte (8.30 \\xe2\\x82\\xac)</a>'
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
'<td>Eboutic</td>\\n <td><a href="/user/3/">Subscribed User</a></td>\\n'
|
|
||||||
" <td>Barbar</td>\\n <td>1</td>\\n <td>1.70 \\xe2\\x82\\xac</td>\\n"
|
|
||||||
" <td>Compte utilisateur</td>" in str(response.content)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_buy_simple_product_with_credit_card(self):
|
def test_buy_with_sith_account_no_money(self):
|
||||||
self.client.login(username="subscriber", password="plop")
|
self.client.login(username="subscriber", password="plop")
|
||||||
response = self.client.post(
|
basket = self.get_busy_basket(self.subscriber)
|
||||||
reverse("eboutic:main"),
|
initial = basket.get_total() - 1
|
||||||
{"action": "add_product", "product_id": self.barbar.id},
|
self.subscriber.customer.amount = initial
|
||||||
)
|
self.subscriber.customer.save()
|
||||||
self.assertTrue(
|
response = self.client.post(reverse("eboutic:pay_with_sith"))
|
||||||
'<input type="hidden" name="action" value="add_product">\\n'
|
self.assertRedirects(response, "/eboutic/pay/failure/")
|
||||||
' <button type="submit" name="product_id" value="4"> + </button>\\n'
|
new_balance = Customer.objects.get(user=self.subscriber).amount
|
||||||
"</form>\\n Barbar: 1.70 \\xe2\\x82\\xac</li>" in str(response.content)
|
self.assertEqual(float(new_balance), initial)
|
||||||
)
|
self.assertEqual(
|
||||||
response = self.client.post(reverse("eboutic:command"))
|
'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic',
|
||||||
self.assertTrue(
|
self.client.cookies["basket_items"].OutputString(),
|
||||||
"<tr>\\n <td>Barbar</td>\\n <td>1</td>\\n"
|
) # this cookie should be removed after payment
|
||||||
" <td>1.70 \\xe2\\x82\\xac</td>\\n </tr>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
def test_submit_basket(self):
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
self.assertTrue(response.content.decode("utf-8") == "")
|
|
||||||
|
|
||||||
response = self.client.get(
|
|
||||||
reverse(
|
|
||||||
"core:user_account_detail",
|
|
||||||
kwargs={
|
|
||||||
"user_id": self.subscriber.id,
|
|
||||||
"year": datetime.now().year,
|
|
||||||
"month": datetime.now().month,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
'class="selected_tab">Compte (0.00 \\xe2\\x82\\xac)</a>'
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
'<td>Eboutic</td>\\n <td><a href="/user/3/">Subscribed User</a></td>\\n'
|
|
||||||
" <td>Barbar</td>\\n <td>1</td>\\n <td>1.70 \\xe2\\x82\\xac</td>\\n"
|
|
||||||
" <td>Carte bancaire</td>" in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_alter_basket_with_credit_card(self):
|
|
||||||
self.client.login(username="subscriber", password="plop")
|
self.client.login(username="subscriber", password="plop")
|
||||||
response = self.client.post(
|
self.client.cookies[
|
||||||
reverse("eboutic:main"),
|
"basket_items"
|
||||||
{"action": "add_product", "product_id": self.barbar.id},
|
] = """[
|
||||||
)
|
{"id": 2, "name": "Cotis 2 semestres", "quantity": 1, "unit_price": 28},
|
||||||
self.assertTrue(
|
{"id": 4, "name": "Barbar", "quantity": 3, "unit_price": 1.7}
|
||||||
'<input type="hidden" name="action" value="add_product">\\n'
|
]"""
|
||||||
' <button type="submit" name="product_id" value="4"> + </button>\\n'
|
|
||||||
"</form>\\n Barbar: 1.70 \\xe2\\x82\\xac</li>" in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.post(reverse("eboutic:command"))
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
self.assertTrue(
|
self.assertEqual(response.status_code, 200)
|
||||||
"<tr>\\n <td>Barbar</td>\\n <td>1</td>\\n"
|
self.assertInHTML(
|
||||||
" <td>1.70 \\xe2\\x82\\xac</td>\\n </tr>"
|
"<tr><td>Cotis 2 semestres</td><td>1</td><td>28.00 €</td></tr>",
|
||||||
in str(response.content)
|
response.content.decode(),
|
||||||
)
|
)
|
||||||
|
self.assertInHTML(
|
||||||
|
"<tr><td>Barbar</td><td>3</td><td>1.70 €</td></tr>",
|
||||||
|
response.content.decode(),
|
||||||
|
)
|
||||||
|
self.assertIn("basket_id", self.client.session)
|
||||||
|
basket = Basket.objects.get(id=self.client.session["basket_id"])
|
||||||
|
self.assertEqual(basket.items.count(), 2)
|
||||||
|
barbar = basket.items.filter(product_name="Barbar").first()
|
||||||
|
self.assertIsNotNone(barbar)
|
||||||
|
self.assertEqual(barbar.quantity, 3)
|
||||||
|
cotis = basket.items.filter(product_name="Cotis 2 semestres").first()
|
||||||
|
self.assertIsNotNone(cotis)
|
||||||
|
self.assertEqual(cotis.quantity, 1)
|
||||||
|
self.assertEqual(basket.get_total(), 3 * 1.7 + 28)
|
||||||
|
|
||||||
response_altered = self.client.post(
|
def test_submit_empty_basket(self):
|
||||||
reverse("eboutic:main"),
|
self.client.login(username="subscriber", password="plop")
|
||||||
{"action": "add_product", "product_id": self.barbar.id},
|
self.client.cookies["basket_items"] = "[]"
|
||||||
)
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
self.assertTrue(
|
self.assertRedirects(response, "/eboutic/")
|
||||||
'<input type="hidden" name="action" value="add_product">\\n'
|
|
||||||
' <button type="submit" name="product_id" value="4"> + </button>\\n'
|
|
||||||
"</form>\\n Barbar: 3.40 \\xe2\\x82\\xac</li>"
|
|
||||||
in str(response_altered.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
def test_submit_invalid_basket(self):
|
||||||
self.assertEqual(response.status_code, 500)
|
self.client.login(username="subscriber", password="plop")
|
||||||
|
max_id = Product.objects.aggregate(res=Max("id"))["res"]
|
||||||
|
self.client.cookies[
|
||||||
|
"basket_items"
|
||||||
|
] = f"""[
|
||||||
|
{{"id": {max_id + 1}, "name": "", "quantity": 1, "unit_price": 28}}
|
||||||
|
]"""
|
||||||
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"Basket processing failed with error: SuspiciousOperation('Basket total and amount do not match'",
|
'basket_items=""',
|
||||||
response.content.decode("utf-8"),
|
self.client.cookies["basket_items"].OutputString(),
|
||||||
)
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"Path=/eboutic",
|
||||||
|
self.client.cookies["basket_items"].OutputString(),
|
||||||
|
)
|
||||||
|
self.assertRedirects(response, "/eboutic/")
|
||||||
|
|
||||||
def test_buy_refill_product_with_credit_card(self):
|
def test_submit_basket_illegal_quantity(self):
|
||||||
self.client.login(username="subscriber", password="plop")
|
self.client.login(username="subscriber", password="plop")
|
||||||
response = self.client.post(
|
self.client.cookies[
|
||||||
reverse("eboutic:main"),
|
"basket_items"
|
||||||
{"action": "add_product", "product_id": self.refill.id},
|
] = """[
|
||||||
)
|
{"id": 4, "name": "Barbar", "quantity": -1, "unit_price": 1.7}
|
||||||
self.assertTrue(
|
]"""
|
||||||
'<input type="hidden" name="action" value="add_product">\\n'
|
|
||||||
' <button type="submit" name="product_id" value="3"> + </button>\\n'
|
|
||||||
"</form>\\n Rechargement 15 \\xe2\\x82\\xac: 15.00 \\xe2\\x82\\xac</li>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.post(reverse("eboutic:command"))
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
self.assertTrue(
|
self.assertRedirects(response, "/eboutic/")
|
||||||
"<tr>\\n <td>Rechargement 15 \\xe2\\x82\\xac</td>\\n <td>1</td>\\n"
|
|
||||||
" <td>15.00 \\xe2\\x82\\xac</td>\\n </tr>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
|
||||||
self.assertTrue(response.status_code == 200)
|
|
||||||
self.assertTrue(response.content.decode("utf-8") == "")
|
|
||||||
|
|
||||||
response = self.client.get(
|
|
||||||
reverse(
|
|
||||||
"core:user_account_detail",
|
|
||||||
kwargs={
|
|
||||||
"user_id": self.subscriber.id,
|
|
||||||
"year": datetime.now().year,
|
|
||||||
"month": datetime.now().month,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
'class="selected_tab">Compte (15.00 \\xe2\\x82\\xac)</a>'
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
"<td>\\n <ul>\\n \\n "
|
|
||||||
"<li>1 x Rechargement 15 \\xe2\\x82\\xac - 15.00 \\xe2\\x82\\xac</li>\\n"
|
|
||||||
" \\n </ul>\\n </td>\\n"
|
|
||||||
" <td>15.00 \\xe2\\x82\\xac</td>" in str(response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_buy_subscribe_product_with_credit_card(self):
|
def test_buy_subscribe_product_with_credit_card(self):
|
||||||
self.client.login(username="old_subscriber", password="plop")
|
self.client.login(username="old_subscriber", password="plop")
|
||||||
@ -247,48 +184,81 @@ class EbouticTest(TestCase):
|
|||||||
reverse("core:user_profile", kwargs={"user_id": self.old_subscriber.id})
|
reverse("core:user_profile", kwargs={"user_id": self.old_subscriber.id})
|
||||||
)
|
)
|
||||||
self.assertTrue("Non cotisant" in str(response.content))
|
self.assertTrue("Non cotisant" in str(response.content))
|
||||||
response = self.client.post(
|
self.client.cookies[
|
||||||
reverse("eboutic:main"),
|
"basket_items"
|
||||||
{"action": "add_product", "product_id": self.cotis.id},
|
] = """[
|
||||||
)
|
{"id": 2, "name": "Cotis 2 semestres", "quantity": 1, "unit_price": 28}
|
||||||
self.assertTrue(
|
]"""
|
||||||
'<input type="hidden" name="action" value="add_product">\\n'
|
|
||||||
' <button type="submit" name="product_id" value="1"> + </button>\\n'
|
|
||||||
"</form>\\n Cotis 1 semestre: 15.00 \\xe2\\x82\\xac</li>"
|
|
||||||
in str(response.content)
|
|
||||||
)
|
|
||||||
response = self.client.post(reverse("eboutic:command"))
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
self.assertTrue(
|
self.assertInHTML(
|
||||||
"<tr>\\n <td>Cotis 1 semestre</td>\\n <td>1</td>\\n"
|
"<tr><td>Cotis 2 semestres</td><td>1</td><td>28.00 €</td></tr>",
|
||||||
" <td>15.00 \\xe2\\x82\\xac</td>\\n </tr>"
|
response.content.decode(),
|
||||||
in str(response.content)
|
|
||||||
)
|
)
|
||||||
|
basket = Basket.objects.get(id=self.client.session["basket_id"])
|
||||||
|
self.assertEqual(basket.items.count(), 1)
|
||||||
|
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
||||||
|
self.assertTrue(response.status_code == 200)
|
||||||
|
self.assertTrue(response.content.decode("utf-8") == "Payment successful")
|
||||||
|
|
||||||
|
subscriber = User.objects.get(id=self.old_subscriber.id)
|
||||||
|
self.assertEqual(subscriber.subscriptions.count(), 2)
|
||||||
|
sub = subscriber.subscriptions.order_by("-subscription_end").first()
|
||||||
|
self.assertTrue(sub.is_valid_now())
|
||||||
|
self.assertEqual(sub.member, subscriber)
|
||||||
|
self.assertEqual(sub.subscription_type, "deux-semestres")
|
||||||
|
self.assertEqual(sub.location, "EBOUTIC")
|
||||||
|
|
||||||
|
def test_buy_refill_product_with_credit_card(self):
|
||||||
|
self.client.login(username="subscriber", password="plop")
|
||||||
|
# basket contains 1 refill item worth 15€
|
||||||
|
self.client.cookies["basket_items"] = json.dumps(
|
||||||
|
[{"id": 3, "name": "Rechargement 15 €", "quantity": 1, "unit_price": 15}]
|
||||||
|
)
|
||||||
|
initial_balance = self.subscriber.customer.amount
|
||||||
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
|
|
||||||
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
||||||
self.assertTrue(response.status_code == 200)
|
self.assertTrue(response.status_code == 200)
|
||||||
self.assertTrue(response.content.decode("utf-8") == "")
|
self.assertTrue(response.content.decode() == "Payment successful")
|
||||||
|
new_balance = Customer.objects.get(user=self.subscriber).amount
|
||||||
|
self.assertEqual(new_balance, initial_balance + 15)
|
||||||
|
|
||||||
response = self.client.get(
|
def test_alter_basket_after_submission(self):
|
||||||
reverse(
|
self.client.login(username="subscriber", password="plop")
|
||||||
"core:user_account_detail",
|
self.client.cookies["basket_items"] = json.dumps(
|
||||||
kwargs={
|
[{"id": 4, "name": "Barbar", "quantity": 1, "unit_price": 1.7}]
|
||||||
"user_id": self.old_subscriber.id,
|
|
||||||
"year": datetime.now().year,
|
|
||||||
"month": datetime.now().month,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
|
self.client.cookies["basket_items"] = json.dumps(
|
||||||
|
[ # alter basket
|
||||||
|
{"id": 4, "name": "Barbar", "quantity": 3, "unit_price": 1.7}
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.client.post(reverse("eboutic:command"))
|
||||||
'class="selected_tab">Compte (0.00 \\xe2\\x82\\xac)</a>'
|
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
||||||
in str(response.content)
|
self.assertEqual(response.status_code, 500)
|
||||||
|
self.assertIn(
|
||||||
|
"Basket processing failed with error: SuspiciousOperation('Basket total and amount do not match'",
|
||||||
|
response.content.decode("utf-8"),
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
|
||||||
"<td>\\n <ul>\\n \\n "
|
def test_buy_simple_product_with_credit_card(self):
|
||||||
"<li>1 x Cotis 1 semestre - 15.00 \\xe2\\x82\\xac</li>\\n"
|
self.client.login(username="subscriber", password="plop")
|
||||||
" \\n </ul>\\n </td>\\n"
|
self.client.cookies["basket_items"] = json.dumps(
|
||||||
" <td>15.00 \\xe2\\x82\\xac</td>" in str(response.content)
|
[{"id": 4, "name": "Barbar", "quantity": 1, "unit_price": 1.7}]
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.post(reverse("eboutic:command"))
|
||||||
reverse("core:user_profile", kwargs={"user_id": self.old_subscriber.id})
|
response = self.generate_bank_valid_answer_from_page_content(response.content)
|
||||||
|
self.assertTrue(response.status_code == 200)
|
||||||
|
self.assertTrue(response.content.decode("utf-8") == "Payment successful")
|
||||||
|
|
||||||
|
selling = (
|
||||||
|
Selling.objects.filter(customer=self.subscriber.customer)
|
||||||
|
.order_by("-date")
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
self.assertTrue("Cotisant jusqu\\'au" in str(response.content))
|
self.assertEqual(selling.payment_method, "CARD")
|
||||||
|
self.assertEqual(selling.quantity, 1)
|
||||||
|
self.assertEqual(selling.unit_price, self.barbar.selling_price)
|
||||||
|
self.assertEqual(selling.counter.type, "EBOUTIC")
|
||||||
|
self.assertEqual(selling.product, self.barbar)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017, 2022
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
|
# - Maréchal <thgirod@hotmail.com>
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
@ -22,17 +23,21 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
from django.urls import re_path
|
from django.urls import path, register_converter
|
||||||
|
|
||||||
from eboutic.views import *
|
from eboutic.views import *
|
||||||
|
from eboutic.converters import PaymentResultConverter
|
||||||
|
|
||||||
|
register_converter(PaymentResultConverter, "res")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Subscription views
|
# Subscription views
|
||||||
re_path(r"^$", EbouticMain.as_view(), name="main"),
|
path("", eboutic_main, name="main"),
|
||||||
re_path(r"^command$", EbouticCommand.as_view(), name="command"),
|
path("command/", EbouticCommand.as_view(), name="command"),
|
||||||
re_path(r"^pay$", EbouticPayWithSith.as_view(), name="pay_with_sith"),
|
path("pay/", pay_with_sith, name="pay_with_sith"),
|
||||||
re_path(
|
path("pay/<res:result>/", payment_result, name="payment_result"),
|
||||||
r"^et_autoanswer$",
|
path(
|
||||||
|
"et_autoanswer/",
|
||||||
EtransactionAutoAnswer.as_view(),
|
EtransactionAutoAnswer.as_view(),
|
||||||
name="etransation_autoanswer",
|
name="etransation_autoanswer",
|
||||||
),
|
),
|
||||||
|
256
eboutic/views.py
256
eboutic/views.py
@ -22,134 +22,99 @@
|
|||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import hmac
|
import sentry_sdk
|
||||||
import base64
|
|
||||||
|
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.views.generic import TemplateView, View
|
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
|
||||||
from django.db import transaction, DataError
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
from django.db import transaction, DatabaseError
|
||||||
|
from django.http import HttpResponse, HttpRequest
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.http import require_GET, require_POST
|
||||||
|
from django.views.generic import TemplateView, View
|
||||||
|
|
||||||
from counter.models import Customer, Counter, ProductType, Selling
|
from counter.models import Customer, Counter, Selling
|
||||||
from eboutic.models import Basket, Invoice, InvoiceItem
|
from eboutic.forms import BasketForm
|
||||||
|
from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products
|
||||||
|
|
||||||
|
|
||||||
class EbouticMain(TemplateView):
|
@login_required
|
||||||
template_name = "eboutic/eboutic_main.jinja"
|
@require_GET
|
||||||
|
def eboutic_main(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Main view of the eboutic application.
|
||||||
|
Return an Http response whose content is of type text/html.
|
||||||
|
The latter represents the page from which a user can see
|
||||||
|
the catalogue of products that he can buy and fill
|
||||||
|
his shopping cart.
|
||||||
|
|
||||||
def make_basket(self, request):
|
The purchasable products are those of the eboutic which
|
||||||
if "basket_id" not in request.session.keys(): # Init the basket session entry
|
belong to a category of products of a product category
|
||||||
self.basket = Basket(user=request.user)
|
(orphan products are inaccessible).
|
||||||
self.basket.save()
|
|
||||||
else:
|
|
||||||
self.basket = Basket.objects.filter(id=request.session["basket_id"]).first()
|
|
||||||
if self.basket is None:
|
|
||||||
self.basket = Basket(user=request.user)
|
|
||||||
self.basket.save()
|
|
||||||
request.session["basket_id"] = self.basket.id
|
|
||||||
request.session.modified = True
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
If the session contains a key-value pair that associates "errors"
|
||||||
if not request.user.is_authenticated:
|
with a list of strings, this pair is removed from the session
|
||||||
return HttpResponseRedirect(
|
and its value displayed to the user when the page is rendered.
|
||||||
reverse_lazy("core:login", args=self.args, kwargs=kwargs)
|
"""
|
||||||
+ "?next="
|
errors = request.session.pop("errors", None)
|
||||||
+ request.path
|
products = get_eboutic_products(request.user)
|
||||||
)
|
context = {
|
||||||
self.object = Counter.objects.filter(type="EBOUTIC").first()
|
"errors": errors,
|
||||||
self.make_basket(request)
|
"products": products,
|
||||||
return super(EbouticMain, self).get(request, *args, **kwargs)
|
"customer_amount": request.user.account_balance,
|
||||||
|
}
|
||||||
|
return render(request, "eboutic/eboutic_main.jinja", context)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("core:login", args=self.args, kwargs=kwargs)
|
|
||||||
+ "?next="
|
|
||||||
+ request.path
|
|
||||||
)
|
|
||||||
self.object = Counter.objects.filter(type="EBOUTIC").first()
|
|
||||||
self.make_basket(request)
|
|
||||||
if "add_product" in request.POST["action"]:
|
|
||||||
self.add_product(request)
|
|
||||||
elif "del_product" in request.POST["action"]:
|
|
||||||
self.del_product(request)
|
|
||||||
return self.render_to_response(self.get_context_data(**kwargs))
|
|
||||||
|
|
||||||
def add_product(self, request):
|
@require_GET
|
||||||
"""Add a product to the basket"""
|
@login_required
|
||||||
try:
|
def payment_result(request, result: str) -> HttpResponse:
|
||||||
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
|
context = {"success": result == "success"}
|
||||||
if not p.buying_groups.exists():
|
return render(request, "eboutic/eboutic_payment_result.jinja", context)
|
||||||
self.basket.add_product(p)
|
|
||||||
for g in p.buying_groups.all():
|
|
||||||
if request.user.is_in_group(g.name):
|
|
||||||
self.basket.add_product(p)
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def del_product(self, request):
|
|
||||||
"""Delete a product from the basket"""
|
|
||||||
try:
|
|
||||||
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
|
|
||||||
self.basket.del_product(p)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
kwargs = super(EbouticMain, self).get_context_data(**kwargs)
|
|
||||||
kwargs["basket"] = self.basket
|
|
||||||
kwargs["eboutic"] = Counter.objects.filter(type="EBOUTIC").first()
|
|
||||||
kwargs["categories"] = ProductType.objects.all()
|
|
||||||
if hasattr(self.request.user, "customer"):
|
|
||||||
kwargs["customer_amount"] = self.request.user.customer.amount
|
|
||||||
else:
|
|
||||||
kwargs["customer_amount"] = None
|
|
||||||
if not self.request.user.was_subscribed:
|
|
||||||
kwargs["categories"] = kwargs["categories"].exclude(
|
|
||||||
id=settings.SITH_PRODUCTTYPE_SUBSCRIPTION
|
|
||||||
)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class EbouticCommand(TemplateView):
|
class EbouticCommand(TemplateView):
|
||||||
template_name = "eboutic/eboutic_makecommand.jinja"
|
template_name = "eboutic/eboutic_makecommand.jinja"
|
||||||
|
|
||||||
|
@method_decorator(login_required)
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
return redirect("eboutic:main")
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("core:login", args=self.args, kwargs=kwargs)
|
|
||||||
+ "?next="
|
|
||||||
+ request.path
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs)
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
@method_decorator(login_required)
|
||||||
if not request.user.is_authenticated:
|
def post(self, request: HttpRequest, *args, **kwargs):
|
||||||
return HttpResponseRedirect(
|
form = BasketForm(request)
|
||||||
reverse_lazy("core:login", args=self.args, kwargs=kwargs)
|
if not form.is_valid():
|
||||||
+ "?next="
|
request.session["errors"] = form.get_error_messages()
|
||||||
+ request.path
|
request.session.modified = True
|
||||||
)
|
res = redirect("eboutic:main")
|
||||||
if "basket_id" not in request.session.keys():
|
res.set_cookie("basket_items", form.get_cleaned_cookie(), path="/eboutic")
|
||||||
return HttpResponseRedirect(
|
return res
|
||||||
reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs)
|
|
||||||
)
|
if "basket_id" in request.session:
|
||||||
self.basket = Basket.objects.filter(id=request.session["basket_id"]).first()
|
basket, _ = Basket.objects.get_or_create(
|
||||||
if self.basket is None:
|
id=request.session["basket_id"], user=request.user
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs)
|
|
||||||
)
|
)
|
||||||
|
basket.clear()
|
||||||
else:
|
else:
|
||||||
kwargs["basket"] = self.basket
|
basket = Basket.objects.create(user=request.user)
|
||||||
|
|
||||||
|
basket.save()
|
||||||
|
eboutique = Counter.objects.get(type="EBOUTIC")
|
||||||
|
for item in json.loads(request.COOKIES["basket_items"]):
|
||||||
|
basket.add_product(
|
||||||
|
eboutique.products.get(id=(item["id"])), item["quantity"]
|
||||||
|
)
|
||||||
|
request.session["basket_id"] = basket.id
|
||||||
|
request.session.modified = True
|
||||||
|
kwargs["basket"] = basket
|
||||||
return self.render_to_response(self.get_context_data(**kwargs))
|
return self.render_to_response(self.get_context_data(**kwargs))
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -162,12 +127,12 @@ class EbouticCommand(TemplateView):
|
|||||||
kwargs["et_request"]["PBX_SITE"] = settings.SITH_EBOUTIC_PBX_SITE
|
kwargs["et_request"]["PBX_SITE"] = settings.SITH_EBOUTIC_PBX_SITE
|
||||||
kwargs["et_request"]["PBX_RANG"] = settings.SITH_EBOUTIC_PBX_RANG
|
kwargs["et_request"]["PBX_RANG"] = settings.SITH_EBOUTIC_PBX_RANG
|
||||||
kwargs["et_request"]["PBX_IDENTIFIANT"] = settings.SITH_EBOUTIC_PBX_IDENTIFIANT
|
kwargs["et_request"]["PBX_IDENTIFIANT"] = settings.SITH_EBOUTIC_PBX_IDENTIFIANT
|
||||||
kwargs["et_request"]["PBX_TOTAL"] = int(self.basket.get_total() * 100)
|
kwargs["et_request"]["PBX_TOTAL"] = int(kwargs["basket"].get_total() * 100)
|
||||||
kwargs["et_request"][
|
kwargs["et_request"][
|
||||||
"PBX_DEVISE"
|
"PBX_DEVISE"
|
||||||
] = 978 # This is Euro. ET support only this value anyway
|
] = 978 # This is Euro. ET support only this value anyway
|
||||||
kwargs["et_request"]["PBX_CMD"] = self.basket.id
|
kwargs["et_request"]["PBX_CMD"] = kwargs["basket"].id
|
||||||
kwargs["et_request"]["PBX_PORTEUR"] = self.basket.user.email
|
kwargs["et_request"]["PBX_PORTEUR"] = kwargs["basket"].user.email
|
||||||
kwargs["et_request"]["PBX_RETOUR"] = "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"
|
kwargs["et_request"]["PBX_RETOUR"] = "Amount:M;BasketID:R;Auto:A;Error:E;Sig:K"
|
||||||
kwargs["et_request"]["PBX_HASH"] = "SHA512"
|
kwargs["et_request"]["PBX_HASH"] = "SHA512"
|
||||||
kwargs["et_request"]["PBX_TYPEPAIEMENT"] = "CARTE"
|
kwargs["et_request"]["PBX_TYPEPAIEMENT"] = "CARTE"
|
||||||
@ -192,41 +157,24 @@ class EbouticCommand(TemplateView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class EbouticPayWithSith(TemplateView):
|
@login_required
|
||||||
template_name = "eboutic/eboutic_payment_result.jinja"
|
@require_POST
|
||||||
|
def pay_with_sith(request):
|
||||||
def post(self, request, *args, **kwargs):
|
basket = Basket.from_session(request.session)
|
||||||
try:
|
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||||
with transaction.atomic():
|
if basket is None or basket.items.filter(type_id=refilling).exists():
|
||||||
if (
|
return redirect("eboutic:main")
|
||||||
"basket_id" not in request.session.keys()
|
c = Customer.objects.filter(user__id=basket.user.id).first()
|
||||||
or not request.user.is_authenticated
|
|
||||||
):
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs)
|
|
||||||
)
|
|
||||||
b = Basket.objects.filter(id=request.session["basket_id"]).first()
|
|
||||||
if (
|
|
||||||
b is None
|
|
||||||
or b.items.filter(
|
|
||||||
type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
|
||||||
).exists()
|
|
||||||
):
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs)
|
|
||||||
)
|
|
||||||
c = Customer.objects.filter(user__id=b.user.id).first()
|
|
||||||
if c is None:
|
if c is None:
|
||||||
return HttpResponseRedirect(
|
return redirect("eboutic:main")
|
||||||
reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs)
|
if c.amount < basket.get_total():
|
||||||
)
|
res = redirect("eboutic:payment_result", "failure")
|
||||||
kwargs["not_enough"] = True
|
|
||||||
if c.amount < b.get_total():
|
|
||||||
raise DataError(_("You do not have enough money to buy the basket"))
|
|
||||||
else:
|
else:
|
||||||
eboutic = Counter.objects.filter(type="EBOUTIC").first()
|
eboutic = Counter.objects.filter(type="EBOUTIC").first()
|
||||||
for it in b.items.all():
|
try:
|
||||||
product = eboutic.products.filter(id=it.product_id).first()
|
with transaction.atomic():
|
||||||
|
for it in basket.items.all():
|
||||||
|
product = eboutic.products.get(id=it.product_id)
|
||||||
Selling(
|
Selling(
|
||||||
label=it.product_name,
|
label=it.product_name,
|
||||||
counter=eboutic,
|
counter=eboutic,
|
||||||
@ -238,16 +186,24 @@ class EbouticPayWithSith(TemplateView):
|
|||||||
quantity=it.quantity,
|
quantity=it.quantity,
|
||||||
payment_method="SITH_ACCOUNT",
|
payment_method="SITH_ACCOUNT",
|
||||||
).save()
|
).save()
|
||||||
b.delete()
|
basket.delete()
|
||||||
kwargs["not_enough"] = False
|
|
||||||
request.session.pop("basket_id", None)
|
request.session.pop("basket_id", None)
|
||||||
except DataError as e:
|
res = redirect("eboutic:payment_result", "success")
|
||||||
kwargs["not_enough"] = True
|
except DatabaseError as e:
|
||||||
return self.render_to_response(self.get_context_data(**kwargs))
|
with sentry_sdk.push_scope() as scope:
|
||||||
|
scope.user = {"username": request.user.username}
|
||||||
|
scope.set_extra("someVariable", e.__repr__())
|
||||||
|
sentry_sdk.capture_message(
|
||||||
|
f"Erreur le {datetime.now()} dans eboutic.pay_with_sith"
|
||||||
|
)
|
||||||
|
res = redirect("eboutic:payment_result", "failure")
|
||||||
|
res.delete_cookie("basket_items", "/eboutic")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
class EtransactionAutoAnswer(View):
|
class EtransactionAutoAnswer(View):
|
||||||
# Response documentation http://www1.paybox.com/espace-integrateur-documentation/la-solution-paybox-system/gestion-de-la-reponse/
|
# Response documentation http://www1.paybox.com/espace-integrateur-documentation
|
||||||
|
# /la-solution-paybox-system/gestion-de-la-reponse/
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if (
|
if (
|
||||||
not "Amount" in request.GET.keys()
|
not "Amount" in request.GET.keys()
|
||||||
@ -305,7 +261,7 @@ class EtransactionAutoAnswer(View):
|
|||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
"Basket processing failed with error: " + repr(e), status=500
|
"Basket processing failed with error: " + repr(e), status=500
|
||||||
)
|
)
|
||||||
return HttpResponse()
|
return HttpResponse("Payment successful", status=200)
|
||||||
else:
|
else:
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
"Payment failed with error: " + request.GET["Error"], status=202
|
"Payment failed with error: " + request.GET["Error"], status=202
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -690,4 +690,5 @@ SITH_FRONT_DEP_VERSIONS = {
|
|||||||
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
|
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
|
||||||
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
||||||
"https://github.com/vuejs/vue-next": "3.2.18",
|
"https://github.com/vuejs/vue-next": "3.2.18",
|
||||||
|
"https://github.com/alpinejs/alpine": "3.10.3",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user