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
dist/
.vscode/
.idea
.idea/
env/
doc/html
data/

View File

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

View File

@ -19,4 +19,4 @@
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// 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;
border-radius: 6px 6px 0 0;
box-shadow: $shadow-color 0 0 15px;
align-items: center;
a {
flex: auto;

View File

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

View File

@ -26,8 +26,10 @@ import json
import re
import typing
from urllib.parse import unquote
from django.http import HttpRequest
from django.utils.translation import gettext as _
from sentry_sdk import capture_message
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 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 ("[]", ""):
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
# malicious user can't DDoS the server with deeply nested json
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."))
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:

View File

@ -63,7 +63,7 @@ document.addEventListener('alpine:init', () => {
edit_cookies() {
// 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 json
from datetime import datetime
import sentry_sdk
from datetime import datetime
from urllib.parse import unquote
from OpenSSL import crypto
from django.conf import settings
from django.contrib.auth.decorators import login_required
@ -104,7 +105,7 @@ class EbouticCommand(TemplateView):
request.session["basket_id"] = basket.id
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"])
ids = [item["id"] for item in items]
quantities = [item["quantity"] for item in items]

View File

@ -100,7 +100,7 @@
</td>
</tr>
<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) %}
<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 '' }}>
@ -118,7 +118,7 @@
{%- endif %}
</td>
{%- 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">
{%- for candidature in election_list.candidatures.filter(role=role) %}
<li class="candidate">
@ -141,7 +141,7 @@
{%- endif %}
</figcaption>
{%- if user.can_edit(candidature) -%}
{% if election.is_vote_editable %}
{%- if election.is_vote_editable -%}
<div class="edit_btns">
<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>

View File

@ -278,8 +278,8 @@ SITH_MAIN_CLUB = {
# Bar managers
SITH_BAR_MANAGER = {
"name": "BdF",
"unix_name": "bdf",
"name": "Pdf",
"unix_name": "pdfesti",
"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_OFFICES = {2: "BdF", 1: "AE"}
SITH_COUNTER_OFFICES = {2: "PdF", 1: "AE"}
SITH_COUNTER_PAYMENT_METHOD = [
("CHECK", _("Check")),