Fix le panier de l'Eboutic pour Safari (#518)

Co-authored-by: Théo DURR <git@theodurr.fr>
Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>
This commit is contained in:
Julien Constant 2022-12-14 08:38:41 +01:00 committed by GitHub
parent 1d82e2a7d9
commit faccc1367f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 46 additions and 19 deletions

2
.gitignore vendored
View File

@ -7,7 +7,7 @@ db.sqlite3
pyrightconfig.json pyrightconfig.json
dist/ dist/
.vscode/ .vscode/
.idea .idea/
env/ env/
doc/html doc/html
data/ data/

View File

@ -33,6 +33,8 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from core.models import User from core.models import User
from club.models import Club, Membership, Mailing from club.models import Club, Membership, Mailing
from club.forms import MailingForm from club.forms import MailingForm
from sith.settings import SITH_BAR_MANAGER
# Create your tests here. # Create your tests here.
@ -43,7 +45,7 @@ class ClubTest(TestCase):
self.skia = User.objects.filter(username="skia").first() self.skia = User.objects.filter(username="skia").first()
self.rbatsbak = User.objects.filter(username="rbatsbak").first() self.rbatsbak = User.objects.filter(username="rbatsbak").first()
self.guy = User.objects.filter(username="guy").first() self.guy = User.objects.filter(username="guy").first()
self.bdf = Club.objects.filter(unix_name="bdf").first() self.bdf = Club.objects.filter(unix_name=SITH_BAR_MANAGER["unix_name"]).first()
def test_create_add_user_to_club_from_root_ok(self): def test_create_add_user_to_club_from_root_ok(self):
self.client.login(username="root", password="plop") self.client.login(username="root", password="plop")
@ -390,7 +392,7 @@ class MailingFormTest(TestCase):
self.rbatsbak = User.objects.filter(username="rbatsbak").first() self.rbatsbak = User.objects.filter(username="rbatsbak").first()
self.krophil = User.objects.filter(username="krophil").first() self.krophil = User.objects.filter(username="krophil").first()
self.comunity = User.objects.filter(username="comunity").first() self.comunity = User.objects.filter(username="comunity").first()
self.bdf = Club.objects.filter(unix_name="bdf").first() self.bdf = Club.objects.filter(unix_name=SITH_BAR_MANAGER["unix_name"]).first()
Membership( Membership(
user=self.rbatsbak, user=self.rbatsbak,
club=self.bdf, club=self.bdf,

View File

@ -19,4 +19,4 @@
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
!function(e){e.fn.shorten=function(s){"use strict";var t={showChars:100,minHideChars:10,ellipsesText:"...",moreText:"more",lessText:"less",onLess:function(){},onMore:function(){},errMsg:null,force:!1};return s&&e.extend(t,s),e(this).data("jquery.shorten")&&!t.force?!1:(e(this).data("jquery.shorten",!0),e(document).off("click",".morelink"),e(document).on({click:function(){var s=e(this);return s.hasClass("less")?(s.removeClass("less"),s.html(t.moreText),s.parent().prev().animate({height:"0%"},function(){s.parent().prev().prev().show()}).hide("fast",function(){t.onLess()})):(s.addClass("less"),s.html(t.lessText),s.parent().prev().animate({height:"100%"},function(){s.parent().prev().prev().hide()}).show("fast",function(){t.onMore()})),!1}},".morelink"),this.each(function(){var s=e(this),n=s.html(),r=s.text().length;if(r>t.showChars+t.minHideChars){var o=n.substr(0,t.showChars);if(o.indexOf("<")>=0){for(var a=!1,i="",h=0,l=[],c=null,f=0,u=0;u<=t.showChars;f++)if("<"!=n[f]||a||(a=!0,c=n.substring(f+1,n.indexOf(">",f)),"/"==c[0]?c!="/"+l[0]?t.errMsg="ERROR en HTML: the top of the stack should be the tag that closes":l.shift():"br"!=c.toLowerCase()&&l.unshift(c)),a&&">"==n[f]&&(a=!1),a)i+=n.charAt(f);else if(u++,h<=t.showChars)i+=n.charAt(f),h++;else if(l.length>0){for(j=0;j<l.length;j++)i+="</"+l[j]+">";break}o=e("<div/>").html(i+'<span class="ellip">'+t.ellipsesText+"</span>").html()}else o+=t.ellipsesText;var m='<div class="shortcontent">'+o+'</div><div class="allcontent">'+n+'</div><span><a href="javascript://nop/" class="morelink">'+t.moreText+"</a></span>";s.html(m),s.find(".allcontent").hide(),e(".shortcontent p:last",s).css("margin-bottom",0)}}))}}(jQuery); !function(e){e.fn.shorten=function(s){"use strict";var t={showChars:100,minHideChars:10,ellipsesText:"...",moreText:"more",lessText:"less",onLess:function(){},onMore:function(){},errMsg:null,force:!1};return s&&e.extend(t,s),(!e(this).data("jquery.shorten")||!!t.force)&&(e(this).data("jquery.shorten",!0),e(document).off("click",".morelink"),e(document).on({click:function(){var s=e(this);return s.hasClass("less")?(s.removeClass("less"),s.html(t.moreText),s.parent().prev().animate({},function(){s.parent().prev().prev().show()}).hide("fast",function(){t.onLess()})):(s.addClass("less"),s.html(t.lessText),s.parent().prev().animate({},function(){s.parent().prev().prev().hide()}).show("fast",function(){t.onMore()})),!1}},".morelink"),this.each(function(){var s=e(this),n=s.html();if(s.text().length>t.showChars+t.minHideChars){var r=n.substr(0,t.showChars);if(r.indexOf("<")>=0){for(var a=!1,o="",i=0,l=[],h=null,c=0,f=0;f<=t.showChars;c++)if("<"!=n[c]||a||(a=!0,"/"==(h=n.substring(c+1,n.indexOf(">",c)))[0]?h!="/"+l[0]?t.errMsg="ERROR en HTML: the top of the stack should be the tag that closes":l.shift():"br"!=h.toLowerCase()&&l.unshift(h)),a&&">"==n[c]&&(a=!1),a)o+=n.charAt(c);else if(f++,i<=t.showChars)o+=n.charAt(c),i++;else if(l.length>0){for(j=0;j<l.length;j++)o+="</"+l[j]+">";break}r=e("<div/>").html(o+'<span class="ellip">'+t.ellipsesText+"</span>").html()}else r+=t.ellipsesText;var p='<div class="shortcontent">'+r+'</div><div class="allcontent">'+n+'</div><span><a href="javascript://nop/" class="morelink">'+t.moreText+"</a></span>";s.html(p),s.find(".allcontent").hide(),e(".shortcontent p:last",s).css("margin-bottom",0)}}))}}(jQuery);

View File

@ -381,6 +381,7 @@ header {
color: $white-color; color: $white-color;
border-radius: 6px 6px 0 0; border-radius: 6px 6px 0 0;
box-shadow: $shadow-color 0 0 15px; box-shadow: $shadow-color 0 0 15px;
align-items: center;
a { a {
flex: auto; flex: auto;

View File

@ -166,14 +166,15 @@ $min_col_width: 100px;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
width: 100%;
gap: $gap; gap: $gap;
>.candidate { >.candidate {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
list-style-type: none; list-style-type: none;
width: 100%;
gap: $gap; gap: $gap;
>input[type="radio"]:checked + label, >input[type="radio"]:checked + label,
@ -186,6 +187,10 @@ $min_col_width: 100px;
} }
} }
>label {
width: 100%;
}
>label>figure, >label>figure,
>figure { >figure {
position: relative; position: relative;
@ -194,17 +199,22 @@ $min_col_width: 100px;
align-items: center; align-items: center;
gap: $gap; gap: $gap;
padding: 10px; padding: 10px;
max-width: 100%;
>img { >img {
max-width: 100%; max-width: 100% !important;
} }
>figcaption { >figcaption {
width: 100%;
max-width: inherit !important;
overflow: hidden;
h5 { h5 {
margin: 0; margin: 0;
text-align: center; text-align: center;
} }
q { .candidate_program {
margin: 5px 0; margin: 5px 0;
} }
} }

View File

@ -26,8 +26,10 @@ import json
import re import re
import typing import typing
from urllib.parse import unquote
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from sentry_sdk import capture_message
from eboutic.models import get_eboutic_products from eboutic.models import get_eboutic_products
@ -97,23 +99,34 @@ class BasketForm:
- all the ids refer to products the user is allowed to buy - all the ids refer to products the user is allowed to buy
- all the quantities are positive integers - all the quantities are positive integers
""" """
basket = self.cookies.get("basket_items", None) basket = unquote(self.cookies.get("basket_items", None))
if basket is None or basket in ("[]", ""): if basket is None or basket in ("[]", ""):
self.error_messages.add(_("You have no basket.")) self.error_messages.add(_("You have no basket."))
return return
# check that the json is not nested before parsing it to make sure # check that the json is not nested before parsing it to make sure
# malicious user can't ddos the server with deeply nested json # malicious user can't DDoS the server with deeply nested json
if not BasketForm.json_cookie_re.match(basket): if not BasketForm.json_cookie_re.match(basket):
# As the validation of the cookie goes through a rather boring regex,
# we can regularly have to deal with subtle errors that we hadn't forecasted,
# so we explicitly lay a Sentry message capture here.
capture_message(
"Eboutic basket regex checking failed to validate basket json",
level="error",
)
self.error_messages.add(_("The request was badly formatted.")) self.error_messages.add(_("The request was badly formatted."))
return return
try: try:
basket = json.loads(basket) basket = json.loads(basket)
except json.JSONDecodeError: except json.JSONDecodeError:
self.error_messages.add(_("The basket cookie was badly formatted.")) self.error_messages.add(_("The basket cookie was badly formatted."))
return return
if type(basket) is not list or len(basket) == 0: if type(basket) is not list or len(basket) == 0:
self.error_messages.add(_("Your basket is empty.")) self.error_messages.add(_("Your basket is empty."))
return return
for item in basket: for item in basket:
expected_keys = {"id", "quantity", "name", "unit_price"} expected_keys = {"id", "quantity", "name", "unit_price"}
if type(item) is not dict or set(item.keys()) != expected_keys: if type(item) is not dict or set(item.keys()) != expected_keys:

View File

@ -63,7 +63,7 @@ document.addEventListener('alpine:init', () => {
edit_cookies() { edit_cookies() {
// a cookie survives an hour // a cookie survives an hour
document.cookie = "basket_items=" + JSON.stringify(this.items) + ";Max-Age=3600"; document.cookie = "basket_items=" + encodeURIComponent(JSON.stringify(this.items)) + ";Max-Age=3600";
}, },
/** /**

View File

@ -24,9 +24,10 @@
import base64 import base64
import json import json
from datetime import datetime
import sentry_sdk import sentry_sdk
from datetime import datetime
from urllib.parse import unquote
from OpenSSL import crypto from OpenSSL import crypto
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -104,7 +105,7 @@ class EbouticCommand(TemplateView):
request.session["basket_id"] = basket.id request.session["basket_id"] = basket.id
request.session.modified = True request.session.modified = True
items = json.loads(request.COOKIES["basket_items"]) items = json.loads(unquote(request.COOKIES["basket_items"]))
items.sort(key=lambda item: item["id"]) items.sort(key=lambda item: item["id"])
ids = [item["id"] for item in items] ids = [item["id"] for item in items]
quantities = [item["quantity"] for item in items] quantities = [item["quantity"] for item in items]

View File

@ -100,7 +100,7 @@
</td> </td>
</tr> </tr>
<tr class="role_candidates"> <tr class="role_candidates">
<td class="list_per_role" style="width: {{ 100 / (election_lists.count() + 1) }}%"> <td class="list_per_role" style="width: 100%; max-width: {{ 100 / (election_lists.count() + 1) }}%">
{%- if role.max_choice == 1 and election.can_vote(user) %} {%- if role.max_choice == 1 and election.can_vote(user) %}
<div class="radio-btn"> <div class="radio-btn">
<input id="id_{{ role.title }}_{{ count[0] }}" type="radio" name="{{ role.title }}" value {{ '' if role_data in election_form else 'checked' }} {{ 'disabled' if election.has_voted(user) else '' }}> <input id="id_{{ role.title }}_{{ count[0] }}" type="radio" name="{{ role.title }}" value {{ '' if role_data in election_form else 'checked' }} {{ 'disabled' if election.has_voted(user) else '' }}>
@ -118,7 +118,7 @@
{%- endif %} {%- endif %}
</td> </td>
{%- for election_list in election_lists %} {%- for election_list in election_lists %}
<td class="list_per_role" style="width: {{ 100 / (election_lists.count() + 1) }}%"> <td class="list_per_role" style="width: 100%; max-width: {{ 100 / (election_lists.count() + 1) }}%">
<ul class="candidates"> <ul class="candidates">
{%- for candidature in election_list.candidatures.filter(role=role) %} {%- for candidature in election_list.candidatures.filter(role=role) %}
<li class="candidate"> <li class="candidate">
@ -141,7 +141,7 @@
{%- endif %} {%- endif %}
</figcaption> </figcaption>
{%- if user.can_edit(candidature) -%} {%- if user.can_edit(candidature) -%}
{% if election.is_vote_editable %} {%- if election.is_vote_editable -%}
<div class="edit_btns"> <div class="edit_btns">
<a href="{{url('election:update_candidate', candidature_id=candidature.id)}}">{% trans %}✏️{% endtrans %}</a> <a href="{{url('election:update_candidate', candidature_id=candidature.id)}}">{% trans %}✏️{% endtrans %}</a>
<a href="{{url('election:delete_candidate', candidature_id=candidature.id)}}">{% trans %}{% endtrans %}</a> <a href="{{url('election:delete_candidate', candidature_id=candidature.id)}}">{% trans %}{% endtrans %}</a>

View File

@ -278,8 +278,8 @@ SITH_MAIN_CLUB = {
# Bar managers # Bar managers
SITH_BAR_MANAGER = { SITH_BAR_MANAGER = {
"name": "BdF", "name": "Pdf",
"unix_name": "bdf", "unix_name": "pdfesti",
"address": "6 Boulevard Anatole France, 90000 Belfort", "address": "6 Boulevard Anatole France, 90000 Belfort",
} }
@ -376,7 +376,7 @@ SITH_SUBSCRIPTION_LOCATIONS = [
SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")] SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")]
SITH_COUNTER_OFFICES = {2: "BdF", 1: "AE"} SITH_COUNTER_OFFICES = {2: "PdF", 1: "AE"}
SITH_COUNTER_PAYMENT_METHOD = [ SITH_COUNTER_PAYMENT_METHOD = [
("CHECK", _("Check")), ("CHECK", _("Check")),