diff --git a/.mailmap b/.mailmap index 6bd29747..33469c51 100644 --- a/.mailmap +++ b/.mailmap @@ -15,4 +15,5 @@ Vial Zar root tleb -tleb \ No newline at end of file +tleb +Maréchal \ No newline at end of file diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 0c661b1d..130a139d 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -505,6 +505,8 @@ Welcome to the wiki page! refound.save() # 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=r, account_id="4000k", amount=0).save() p = ProductType(name="Bières bouteilles") @@ -525,6 +527,9 @@ Welcome to the wiki page! club=main_club, ) cotis.save() + cotis.buying_groups.add(subscribers) + cotis.buying_groups.add(old_subscribers) + cotis.save() cotis2 = Product( name="Cotis 2 semestres", code="2SCOTIZ", @@ -535,6 +540,9 @@ Welcome to the wiki page! club=main_club, ) cotis2.save() + cotis2.buying_groups.add(subscribers) + cotis2.buying_groups.add(old_subscribers) + cotis2.save() refill = Product( name="Rechargement 15 €", code="15REFILL", @@ -545,6 +553,8 @@ Welcome to the wiki page! club=main_club, ) refill.save() + refill.buying_groups.add(subscribers) + refill.save() barb = Product( name="Barbar", code="BARB", @@ -553,8 +563,11 @@ Welcome to the wiki page! selling_price="1.7", special_selling_price="1.6", club=main_club, + limit_age=18, ) barb.save() + barb.buying_groups.add(subscribers) + barb.save() cble = Product( name="Chimay Bleue", code="CBLE", @@ -563,8 +576,11 @@ Welcome to the wiki page! selling_price="1.7", special_selling_price="1.6", club=main_club, + limit_age=18, ) cble.save() + cble.buying_groups.add(subscribers) + cble.save() cons = Product( name="Consigne Eco-cup", code="CONS", @@ -574,7 +590,6 @@ Welcome to the wiki page! special_selling_price="1", club=main_club, ) - cons.id = 1152 cons.save() dcons = Product( name="Déconsigne Eco-cup", @@ -585,9 +600,8 @@ Welcome to the wiki page! special_selling_price="-1", club=main_club, ) - dcons.id = 1151 dcons.save() - Product( + cors = Product( name="Corsendonk", code="CORS", product_type=p, @@ -595,8 +609,12 @@ Welcome to the wiki page! selling_price="1.7", special_selling_price="1.6", club=main_club, - ).save() - Product( + limit_age=18, + ) + cors.save() + cors.buying_groups.add(subscribers) + cors.save() + carolus = Product( name="Carolus", code="CARO", product_type=p, @@ -604,7 +622,11 @@ Welcome to the wiki page! selling_price="1.7", special_selling_price="1.6", club=main_club, - ).save() + limit_age=18, + ) + carolus.save() + carolus.buying_groups.add(subscribers) + carolus.save() mde = Counter.objects.filter(name="MDE").first() mde.products.add(barb) mde.products.add(cble) diff --git a/core/models.py b/core/models.py index 1d1d4266..0d0dc0b7 100644 --- a/core/models.py +++ b/core/models.py @@ -325,6 +325,13 @@ class User(AbstractBaseUser): ) return s.exists() + @cached_property + def account_balance(self): + if hasattr(self, "customer"): + return self.customer.amount + else: + return 0 + _club_memberships = {} _group_names = {} _group_ids = {} @@ -459,6 +466,20 @@ class User(AbstractBaseUser): def is_banned_counter(self): 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): create = False with transaction.atomic(): diff --git a/core/static/core/js/alpinejs.min.js b/core/static/core/js/alpinejs.min.js new file mode 100644 index 00000000..b1337926 --- /dev/null +++ b/core/static/core/js/alpinejs.min.js @@ -0,0 +1,5 @@ +(()=>{var We=!1,Ge=!1,B=[];function $t(e){an(e)}function an(e){B.includes(e)||B.push(e),cn()}function he(e){let t=B.indexOf(e);t!==-1&&B.splice(t,1)}function cn(){!Ge&&!We&&(We=!0,queueMicrotask(ln))}function ln(){We=!1,Ge=!0;for(let e=0;ee.effect(t,{scheduler:r=>{Je?$t(r):r()}}),Ye=e.raw}function Ze(e){K=e}function Ft(e){let t=()=>{};return[n=>{let i=K(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),Y(i))},i},()=>{t()}]}var Bt=[],Kt=[],zt=[];function Vt(e){zt.push(e)}function _e(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Kt.push(t))}function Ht(e){Bt.push(e)}function qt(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Qe(e,t){!e._x_attributeCleanups||Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var et=new MutationObserver(Xe),tt=!1;function rt(){et.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),tt=!0}function fn(){un(),et.disconnect(),tt=!1}var te=[],nt=!1;function un(){te=te.concat(et.takeRecords()),te.length&&!nt&&(nt=!0,queueMicrotask(()=>{dn(),nt=!1}))}function dn(){Xe(te),te.length=0}function m(e){if(!tt)return e();fn();let t=e();return rt(),t}var it=!1,ge=[];function Ut(){it=!0}function Wt(){it=!1,Xe(ge),ge=[]}function Xe(e){if(it){ge=ge.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{Qe(s,o)}),n.forEach((o,s)=>{Bt.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(Kt.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||!o.isConnected||(delete o._x_ignoreSelf,delete o._x_ignore,zt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function xe(e){return D(k(e))}function C(e,t,r){return e._x_dataStack=[t,...k(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function ot(e,t){let r=e._x_dataStack[0];Object.entries(t).forEach(([n,i])=>{r[n]=i})}function k(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?k(e.host):e.parentNode?k(e.parentNode):[]}function D(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,c=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...c,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function ye(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function be(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>pn(n,i),s=>st(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function st(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),st(e[t[0]],t.slice(1),r)}}var Gt={};function x(e,t){Gt[e]=t}function re(e,t){return Object.entries(Gt).forEach(([r,n])=>{Object.defineProperty(e,`$${r}`,{get(){let[i,o]=at(t);return i={interceptor:be,...i},_e(t,o),n(t,i)},enumerable:!1})}),e}function Yt(e,t,r,...n){try{return r(...n)}catch(i){J(i,e,t)}}function J(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} + +${r?'Expression: "'+r+`" + +`:""}`,t),setTimeout(()=>{throw e},0)}var ve=!0;function Jt(e){let t=ve;ve=!1,e(),ve=t}function P(e,t,r={}){let n;return g(e,t)(i=>n=i,r),n}function g(...e){return Zt(...e)}var Zt=ct;function Qt(e){Zt=e}function ct(e,t){let r={};re(r,e);let n=[r,...k(e)];if(typeof t=="function")return mn(n,t);let i=hn(n,t,e);return Yt.bind(null,e,t,i)}function mn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(D([n,...e]),i);we(r,o)}}var lt={};function _n(e,t){if(lt[e])return lt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return J(s,t,e),Promise.resolve()}})();return lt[e]=o,o}function hn(e,t,r){let n=_n(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=D([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>J(l,r,t));n.finished?(we(i,n.result,a,s,r),n.result=void 0):c.then(l=>{we(i,l,a,s,r)}).catch(l=>J(l,r,t)).finally(()=>n.result=void 0)}}}function we(e,t,r,n,i){if(ve&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>we(e,s,r,n)).catch(s=>J(s,i,t)):e(o)}else e(t)}var ut="x-";function E(e=""){return ut+e}function Xt(e){ut=e}var er={};function d(e,t){er[e]=t}function ne(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=ft(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(tr((o,s)=>n[o]=s)).filter(rr).map(xn(n,r)).sort(yn).map(o=>gn(e,o))}function ft(e){return Array.from(e).map(tr()).filter(t=>!rr(t))}var dt=!1,ie=new Map,nr=Symbol();function ir(e){dt=!0;let t=Symbol();nr=t,ie.set(t,[]);let r=()=>{for(;ie.get(t).length;)ie.get(t).shift()();ie.delete(t)},n=()=>{dt=!1,r()};e(r),n()}function at(e){let t=[],r=a=>t.push(a),[n,i]=Ft(e);return t.push(i),[{Alpine:I,effect:n,cleanup:r,evaluateLater:g.bind(g,e),evaluate:P.bind(P,e)},()=>t.forEach(a=>a())]}function gn(e,t){let r=()=>{},n=er[t.type]||r,[i,o]=at(e);qt(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),dt?ie.get(nr).push(n):n())};return s.runCleanups=o,s}var Ee=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Se=e=>e;function tr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=or.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var or=[];function Z(e){or.push(e)}function rr({name:e}){return sr().test(e)}var sr=()=>new RegExp(`^${ut}([^:^.]+)\\b`);function xn(e,t){return({name:r,value:n})=>{let i=r.match(sr()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var pt="DEFAULT",Ae=["ignore","ref","data","id","bind","init","for","mask","model","modelable","transition","show","if",pt,"teleport"];function yn(e,t){let r=Ae.indexOf(e.type)===-1?pt:e.type,n=Ae.indexOf(t.type)===-1?pt:t.type;return Ae.indexOf(r)-Ae.indexOf(n)}function z(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}var mt=[],ht=!1;function Te(e=()=>{}){return queueMicrotask(()=>{ht||setTimeout(()=>{Oe()})}),new Promise(t=>{mt.push(()=>{e(),t()})})}function Oe(){for(ht=!1;mt.length;)mt.shift()()}function ar(){ht=!0}function R(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>R(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)R(n,t,!1),n=n.nextElementSibling}function O(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}function lr(){document.body||O("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` @@ -18,6 +21,8 @@ + {% block additional_css %}{% endblock %} + {% block additional_js %}{% endblock %} {% endblock %} @@ -39,13 +44,12 @@ {% endfor %} - - {% if not user.is_authenticated %} + {% if not user.is_authenticated %} {% else %} -
    {% cache 100 "counters_activity" %} @@ -185,7 +184,9 @@
{% trans %}Forum{% endtrans %} {% trans %}Gallery{% endtrans %} - {% trans %}Eboutic{% endtrans %} + {% if request.user.is_authenticated %} + {% trans %}Eboutic{% endtrans %} + {% endif %} - {% endblock %} diff --git a/eboutic/templates/eboutic/eboutic_payment_result.jinja b/eboutic/templates/eboutic/eboutic_payment_result.jinja index d78e01a3..39d9277a 100644 --- a/eboutic/templates/eboutic/eboutic_payment_result.jinja +++ b/eboutic/templates/eboutic/eboutic_payment_result.jinja @@ -4,14 +4,16 @@

{% trans %}Eboutic{% endtrans %}

- {% if not_enough %} - {% trans %}Payment failed{% endtrans %} - {% else %} + {% if success %} {% trans %}Payment successful{% endtrans %} + {% else %} + {% trans %}Payment failed{% endtrans %} {% endif %}

{% trans %}Return to eboutic{% endtrans %}

+ + {% endblock %} diff --git a/eboutic/tests.py b/eboutic/tests.py index 6435f49b..98b73014 100644 --- a/eboutic/tests.py +++ b/eboutic/tests.py @@ -2,6 +2,7 @@ # # Copyright 2016,2017 # - Skia +# - Maréchal \\n' - ' \\n' - "\\n Barbar: 1.70 \\xe2\\x82\\xac" in str(response.content) - ) - response = self.client.post(reverse("eboutic:command")) - self.assertTrue( - "\\n Barbar\\n 1\\n" - " 1.70 \\xe2\\x82\\xac\\n " - 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)' - in str(response.content) - ) - self.assertTrue( - 'Eboutic\\n Subscribed User\\n' - " Barbar\\n 1\\n 1.70 \\xe2\\x82\\xac\\n" - " Compte utilisateur" in str(response.content) + self.subscriber.customer.amount = 100 # give money before test + self.subscriber.customer.save() + basket = self.get_busy_basket(self.subscriber) + amount = basket.get_total() + response = self.client.post(reverse("eboutic:pay_with_sith")) + self.assertRedirects(response, "/eboutic/pay/success/") + new_balance = Customer.objects.get(user=self.subscriber).amount + self.assertEqual(float(new_balance), 100 - amount) + self.assertEqual( + 'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic', + self.client.cookies["basket_items"].OutputString(), ) - 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") - response = self.client.post( - reverse("eboutic:main"), - {"action": "add_product", "product_id": self.barbar.id}, - ) - self.assertTrue( - '\\n' - ' \\n' - "\\n Barbar: 1.70 \\xe2\\x82\\xac" in str(response.content) - ) - response = self.client.post(reverse("eboutic:command")) - self.assertTrue( - "\\n Barbar\\n 1\\n" - " 1.70 \\xe2\\x82\\xac\\n " - in str(response.content) - ) + basket = self.get_busy_basket(self.subscriber) + initial = basket.get_total() - 1 + self.subscriber.customer.amount = initial + self.subscriber.customer.save() + response = self.client.post(reverse("eboutic:pay_with_sith")) + self.assertRedirects(response, "/eboutic/pay/failure/") + new_balance = Customer.objects.get(user=self.subscriber).amount + self.assertEqual(float(new_balance), initial) + self.assertEqual( + 'basket_items=""; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/eboutic', + self.client.cookies["basket_items"].OutputString(), + ) # this cookie should be removed after payment - 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 (0.00 \\xe2\\x82\\xac)' - in str(response.content) - ) - self.assertTrue( - 'Eboutic\\n Subscribed User\\n' - " Barbar\\n 1\\n 1.70 \\xe2\\x82\\xac\\n" - " Carte bancaire" in str(response.content) - ) - - def test_alter_basket_with_credit_card(self): + def test_submit_basket(self): self.client.login(username="subscriber", password="plop") - response = self.client.post( - reverse("eboutic:main"), - {"action": "add_product", "product_id": self.barbar.id}, - ) - self.assertTrue( - '\\n' - ' \\n' - "\\n Barbar: 1.70 \\xe2\\x82\\xac" in str(response.content) - ) + self.client.cookies[ + "basket_items" + ] = """[ + {"id": 2, "name": "Cotis 2 semestres", "quantity": 1, "unit_price": 28}, + {"id": 4, "name": "Barbar", "quantity": 3, "unit_price": 1.7} + ]""" response = self.client.post(reverse("eboutic:command")) - self.assertTrue( - "\\n Barbar\\n 1\\n" - " 1.70 \\xe2\\x82\\xac\\n " - in str(response.content) + self.assertEqual(response.status_code, 200) + self.assertInHTML( + "Cotis 2 semestres128.00 €", + response.content.decode(), ) + self.assertInHTML( + "Barbar31.70 €", + 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( - reverse("eboutic:main"), - {"action": "add_product", "product_id": self.barbar.id}, - ) - self.assertTrue( - '\\n' - ' \\n' - "\\n Barbar: 3.40 \\xe2\\x82\\xac" - in str(response_altered.content) - ) + def test_submit_empty_basket(self): + self.client.login(username="subscriber", password="plop") + self.client.cookies["basket_items"] = "[]" + response = self.client.post(reverse("eboutic:command")) + self.assertRedirects(response, "/eboutic/") - response = self.generate_bank_valid_answer_from_page_content(response.content) - self.assertEqual(response.status_code, 500) + def test_submit_invalid_basket(self): + 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( - "Basket processing failed with error: SuspiciousOperation('Basket total and amount do not match'", - response.content.decode("utf-8"), + 'basket_items=""', + 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") - response = self.client.post( - reverse("eboutic:main"), - {"action": "add_product", "product_id": self.refill.id}, - ) - self.assertTrue( - '\\n' - ' \\n' - "\\n Rechargement 15 \\xe2\\x82\\xac: 15.00 \\xe2\\x82\\xac" - in str(response.content) - ) + self.client.cookies[ + "basket_items" + ] = """[ + {"id": 4, "name": "Barbar", "quantity": -1, "unit_price": 1.7} + ]""" response = self.client.post(reverse("eboutic:command")) - self.assertTrue( - "\\n Rechargement 15 \\xe2\\x82\\xac\\n 1\\n" - " 15.00 \\xe2\\x82\\xac\\n " - 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)' - in str(response.content) - ) - self.assertTrue( - "\\n
    \\n \\n " - "
  • 1 x Rechargement 15 \\xe2\\x82\\xac - 15.00 \\xe2\\x82\\xac
  • \\n" - " \\n
\\n \\n" - " 15.00 \\xe2\\x82\\xac" in str(response.content) - ) + self.assertRedirects(response, "/eboutic/") def test_buy_subscribe_product_with_credit_card(self): 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}) ) self.assertTrue("Non cotisant" in str(response.content)) - response = self.client.post( - reverse("eboutic:main"), - {"action": "add_product", "product_id": self.cotis.id}, - ) - self.assertTrue( - '\\n' - ' \\n' - "\\n Cotis 1 semestre: 15.00 \\xe2\\x82\\xac" - in str(response.content) - ) + self.client.cookies[ + "basket_items" + ] = """[ + {"id": 2, "name": "Cotis 2 semestres", "quantity": 1, "unit_price": 28} + ]""" response = self.client.post(reverse("eboutic:command")) - self.assertTrue( - "\\n Cotis 1 semestre\\n 1\\n" - " 15.00 \\xe2\\x82\\xac\\n " - in str(response.content) + self.assertInHTML( + "Cotis 2 semestres128.00 €", + response.content.decode(), ) + 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) 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( - reverse( - "core:user_account_detail", - kwargs={ - "user_id": self.old_subscriber.id, - "year": datetime.now().year, - "month": datetime.now().month, - }, - ) + def test_alter_basket_after_submission(self): + self.client.login(username="subscriber", password="plop") + self.client.cookies["basket_items"] = json.dumps( + [{"id": 4, "name": "Barbar", "quantity": 1, "unit_price": 1.7}] ) - self.assertTrue( - 'class="selected_tab">Compte (0.00 \\xe2\\x82\\xac)' - in str(response.content) + 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( - "\\n
    \\n \\n " - "
  • 1 x Cotis 1 semestre - 15.00 \\xe2\\x82\\xac
  • \\n" - " \\n
\\n \\n" - " 15.00 \\xe2\\x82\\xac" in str(response.content) + self.client.post(reverse("eboutic:command")) + response = self.generate_bank_valid_answer_from_page_content(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"), ) - response = self.client.get( - reverse("core:user_profile", kwargs={"user_id": self.old_subscriber.id}) + + def test_buy_simple_product_with_credit_card(self): + self.client.login(username="subscriber", password="plop") + self.client.cookies["basket_items"] = json.dumps( + [{"id": 4, "name": "Barbar", "quantity": 1, "unit_price": 1.7}] ) - self.assertTrue("Cotisant jusqu\\'au" in str(response.content)) + response = self.client.post(reverse("eboutic:command")) + 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.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) diff --git a/eboutic/urls.py b/eboutic/urls.py index a072efb4..f1d882e7 100644 --- a/eboutic/urls.py +++ b/eboutic/urls.py @@ -1,7 +1,8 @@ # -*- coding:utf-8 -* # -# Copyright 2016,2017 +# Copyright 2016,2017, 2022 # - Skia +# - Maréchal # # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # 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.converters import PaymentResultConverter + +register_converter(PaymentResultConverter, "res") urlpatterns = [ # Subscription views - re_path(r"^$", EbouticMain.as_view(), name="main"), - re_path(r"^command$", EbouticCommand.as_view(), name="command"), - re_path(r"^pay$", EbouticPayWithSith.as_view(), name="pay_with_sith"), - re_path( - r"^et_autoanswer$", + path("", eboutic_main, name="main"), + path("command/", EbouticCommand.as_view(), name="command"), + path("pay/", pay_with_sith, name="pay_with_sith"), + path("pay//", payment_result, name="payment_result"), + path( + "et_autoanswer/", EtransactionAutoAnswer.as_view(), name="etransation_autoanswer", ), diff --git a/eboutic/views.py b/eboutic/views.py index 3f2ef6cb..b478397a 100644 --- a/eboutic/views.py +++ b/eboutic/views.py @@ -22,134 +22,99 @@ # # +import base64 +import hmac +import json from collections import OrderedDict from datetime import datetime -import hmac -import base64 +import sentry_sdk + + 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.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 eboutic.models import Basket, Invoice, InvoiceItem +from counter.models import Customer, Counter, Selling +from eboutic.forms import BasketForm +from eboutic.models import Basket, Invoice, InvoiceItem, get_eboutic_products -class EbouticMain(TemplateView): - template_name = "eboutic/eboutic_main.jinja" +@login_required +@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): - if "basket_id" not in request.session.keys(): # Init the basket session entry - self.basket = Basket(user=request.user) - 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 + The purchasable products are those of the eboutic which + belong to a category of products of a product category + (orphan products are inaccessible). - def get(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) - return super(EbouticMain, self).get(request, *args, **kwargs) + If the session contains a key-value pair that associates "errors" + with a list of strings, this pair is removed from the session + and its value displayed to the user when the page is rendered. + """ + errors = request.session.pop("errors", None) + products = get_eboutic_products(request.user) + context = { + "errors": errors, + "products": products, + "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): - """Add a product to the basket""" - try: - p = self.object.products.filter(id=int(request.POST["product_id"])).first() - if not p.buying_groups.exists(): - 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 +@require_GET +@login_required +def payment_result(request, result: str) -> HttpResponse: + context = {"success": result == "success"} + return render(request, "eboutic/eboutic_payment_result.jinja", context) class EbouticCommand(TemplateView): template_name = "eboutic/eboutic_makecommand.jinja" + @method_decorator(login_required) def get(self, request, *args, **kwargs): - if not request.user.is_authenticated: - 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) - ) + return redirect("eboutic:main") - 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 - ) - if "basket_id" not in request.session.keys(): - return HttpResponseRedirect( - reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs) - ) - self.basket = Basket.objects.filter(id=request.session["basket_id"]).first() - if self.basket is None: - return HttpResponseRedirect( - reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs) + @method_decorator(login_required) + def post(self, request: HttpRequest, *args, **kwargs): + form = BasketForm(request) + if not form.is_valid(): + request.session["errors"] = form.get_error_messages() + request.session.modified = True + res = redirect("eboutic:main") + res.set_cookie("basket_items", form.get_cleaned_cookie(), path="/eboutic") + return res + + if "basket_id" in request.session: + basket, _ = Basket.objects.get_or_create( + id=request.session["basket_id"], user=request.user ) + basket.clear() 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)) 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_RANG"] = settings.SITH_EBOUTIC_PBX_RANG 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"][ "PBX_DEVISE" ] = 978 # This is Euro. ET support only this value anyway - kwargs["et_request"]["PBX_CMD"] = self.basket.id - kwargs["et_request"]["PBX_PORTEUR"] = self.basket.user.email + kwargs["et_request"]["PBX_CMD"] = kwargs["basket"].id + 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_HASH"] = "SHA512" kwargs["et_request"]["PBX_TYPEPAIEMENT"] = "CARTE" @@ -192,62 +157,53 @@ class EbouticCommand(TemplateView): return kwargs -class EbouticPayWithSith(TemplateView): - template_name = "eboutic/eboutic_payment_result.jinja" - - def post(self, request, *args, **kwargs): +@login_required +@require_POST +def pay_with_sith(request): + basket = Basket.from_session(request.session) + refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING + if basket is None or basket.items.filter(type_id=refilling).exists(): + return redirect("eboutic:main") + c = Customer.objects.filter(user__id=basket.user.id).first() + if c is None: + return redirect("eboutic:main") + if c.amount < basket.get_total(): + res = redirect("eboutic:payment_result", "failure") + else: + eboutic = Counter.objects.filter(type="EBOUTIC").first() try: with transaction.atomic(): - if ( - "basket_id" not in request.session.keys() - 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: - return HttpResponseRedirect( - reverse_lazy("eboutic:main", args=self.args, kwargs=kwargs) - ) - kwargs["not_enough"] = True - if c.amount < b.get_total(): - raise DataError(_("You do not have enough money to buy the basket")) - else: - eboutic = Counter.objects.filter(type="EBOUTIC").first() - for it in b.items.all(): - product = eboutic.products.filter(id=it.product_id).first() - Selling( - label=it.product_name, - counter=eboutic, - club=product.club, - product=product, - seller=c.user, - customer=c, - unit_price=it.product_unit_price, - quantity=it.quantity, - payment_method="SITH_ACCOUNT", - ).save() - b.delete() - kwargs["not_enough"] = False - request.session.pop("basket_id", None) - except DataError as e: - kwargs["not_enough"] = True - return self.render_to_response(self.get_context_data(**kwargs)) + for it in basket.items.all(): + product = eboutic.products.get(id=it.product_id) + Selling( + label=it.product_name, + counter=eboutic, + club=product.club, + product=product, + seller=c.user, + customer=c, + unit_price=it.product_unit_price, + quantity=it.quantity, + payment_method="SITH_ACCOUNT", + ).save() + basket.delete() + request.session.pop("basket_id", None) + res = redirect("eboutic:payment_result", "success") + except DatabaseError as e: + 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): - # 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): if ( not "Amount" in request.GET.keys() @@ -305,7 +261,7 @@ class EtransactionAutoAnswer(View): return HttpResponse( "Basket processing failed with error: " + repr(e), status=500 ) - return HttpResponse() + return HttpResponse("Payment successful", status=200) else: return HttpResponse( "Payment failed with error: " + request.GET["Error"], status=202 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 9881cb3c..aeb2777a 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-07 16:44+0200\n" +"POT-Creation-Date: 2022-10-10 18:47+0200\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Skia \n" "Language-Team: AE info \n" @@ -18,8 +18,8 @@ msgstr "" #: accounting/models.py:61 accounting/models.py:110 accounting/models.py:143 #: accounting/models.py:216 club/models.py:48 com/models.py:279 -#: com/models.py:296 counter/models.py:122 counter/models.py:150 -#: counter/models.py:214 forum/models.py:58 launderette/models.py:38 +#: com/models.py:296 counter/models.py:131 counter/models.py:159 +#: counter/models.py:243 forum/models.py:58 launderette/models.py:38 #: launderette/models.py:93 launderette/models.py:131 stock/models.py:40 #: stock/models.py:63 stock/models.py:105 stock/models.py:133 msgid "name" @@ -66,8 +66,8 @@ msgid "account number" msgstr "numero de compte" #: accounting/models.py:116 accounting/models.py:147 club/models.py:275 -#: com/models.py:75 com/models.py:266 com/models.py:302 counter/models.py:168 -#: counter/models.py:216 trombi/models.py:217 +#: com/models.py:75 com/models.py:266 com/models.py:302 counter/models.py:177 +#: counter/models.py:245 trombi/models.py:217 msgid "club" msgstr "club" @@ -88,12 +88,12 @@ msgstr "Compte club" msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:214 club/models.py:281 counter/models.py:650 +#: accounting/models.py:214 club/models.py:281 counter/models.py:679 #: election/models.py:18 launderette/models.py:194 msgid "start date" msgstr "date de début" -#: accounting/models.py:215 club/models.py:282 counter/models.py:651 +#: accounting/models.py:215 club/models.py:282 counter/models.py:680 #: election/models.py:19 msgid "end date" msgstr "date de fin" @@ -106,8 +106,8 @@ msgstr "est fermé" msgid "club account" msgstr "compte club" -#: accounting/models.py:225 accounting/models.py:289 counter/models.py:57 -#: counter/models.py:372 +#: accounting/models.py:225 accounting/models.py:289 counter/models.py:56 +#: counter/models.py:401 msgid "amount" msgstr "montant" @@ -127,20 +127,20 @@ msgstr "numéro" msgid "journal" msgstr "classeur" -#: accounting/models.py:290 core/models.py:850 core/models.py:1388 -#: core/models.py:1436 core/models.py:1465 core/models.py:1489 -#: counter/models.py:382 counter/models.py:475 counter/models.py:680 -#: eboutic/models.py:47 eboutic/models.py:100 forum/models.py:311 +#: accounting/models.py:290 core/models.py:889 core/models.py:1427 +#: core/models.py:1475 core/models.py:1504 core/models.py:1528 +#: counter/models.py:411 counter/models.py:504 counter/models.py:709 +#: eboutic/models.py:76 eboutic/models.py:162 forum/models.py:311 #: forum/models.py:408 stock/models.py:104 msgid "date" msgstr "date" -#: accounting/models.py:291 counter/models.py:124 counter/models.py:681 +#: accounting/models.py:291 counter/models.py:133 counter/models.py:710 #: pedagogy/models.py:219 stock/models.py:107 msgid "comment" msgstr "commentaire" -#: accounting/models.py:293 counter/models.py:384 counter/models.py:477 +#: accounting/models.py:293 counter/models.py:413 counter/models.py:506 #: subscription/models.py:66 msgid "payment method" msgstr "méthode de paiement" @@ -149,7 +149,7 @@ msgstr "méthode de paiement" msgid "cheque number" msgstr "numéro de chèque" -#: accounting/models.py:303 eboutic/models.py:185 +#: accounting/models.py:303 eboutic/models.py:247 msgid "invoice" msgstr "facture" @@ -166,8 +166,8 @@ msgid "accounting type" msgstr "type comptable" #: accounting/models.py:328 accounting/models.py:475 accounting/models.py:510 -#: accounting/models.py:545 core/models.py:1464 core/models.py:1490 -#: counter/models.py:441 +#: accounting/models.py:545 core/models.py:1503 core/models.py:1529 +#: counter/models.py:470 msgid "label" msgstr "étiquette" @@ -266,7 +266,7 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:467 counter/models.py:160 pedagogy/models.py:46 +#: accounting/models.py:467 counter/models.py:169 pedagogy/models.py:46 msgid "code" msgstr "code" @@ -1045,8 +1045,8 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:267 counter/models.py:641 counter/models.py:671 -#: eboutic/models.py:43 eboutic/models.py:96 election/models.py:192 +#: club/models.py:267 counter/models.py:670 counter/models.py:700 +#: eboutic/models.py:72 eboutic/models.py:158 election/models.py:192 #: launderette/models.py:145 launderette/models.py:213 sas/models.py:244 #: trombi/models.py:213 msgid "user" @@ -1057,8 +1057,8 @@ msgstr "nom d'utilisateur" msgid "role" msgstr "rôle" -#: club/models.py:289 core/models.py:81 counter/models.py:123 -#: counter/models.py:151 election/models.py:15 election/models.py:120 +#: club/models.py:289 core/models.py:81 counter/models.py:132 +#: counter/models.py:160 election/models.py:15 election/models.py:120 #: election/models.py:197 forum/models.py:59 forum/models.py:240 msgid "description" msgstr "description" @@ -1076,7 +1076,7 @@ msgid "Enter a valid address. Only the root of the address is needed." msgstr "" "Entrez une adresse valide. Seule la racine de l'adresse est nécessaire." -#: club/models.py:353 com/models.py:83 com/models.py:312 core/models.py:851 +#: club/models.py:353 com/models.py:83 com/models.py:312 core/models.py:890 msgid "is moderated" msgstr "est modéré" @@ -1427,7 +1427,7 @@ msgstr "résumé" msgid "content" msgstr "contenu" -#: com/models.py:72 core/models.py:1434 launderette/models.py:101 +#: com/models.py:72 core/models.py:1473 launderette/models.py:101 #: launderette/models.py:139 launderette/models.py:196 stock/models.py:80 #: stock/models.py:137 msgid "type" @@ -1478,7 +1478,7 @@ msgstr "weekmail" msgid "rank" msgstr "rang" -#: com/models.py:298 core/models.py:816 core/models.py:866 +#: com/models.py:298 core/models.py:855 core/models.py:905 msgid "file" msgstr "fichier" @@ -2186,11 +2186,11 @@ msgstr "adresse des parents" msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:505 +#: core/models.py:540 msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:643 core/templates/core/macros.jinja:75 +#: core/models.py:678 core/templates/core/macros.jinja:75 #: core/templates/core/macros.jinja:77 core/templates/core/macros.jinja:78 #: core/templates/core/user_detail.jinja:87 #: core/templates/core/user_detail.jinja:88 @@ -2209,109 +2209,109 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" msgid "Profile" msgstr "Profil" -#: core/models.py:767 +#: core/models.py:806 msgid "Visitor" msgstr "Visiteur" -#: core/models.py:775 +#: core/models.py:814 msgid "do you want to receive the weekmail" msgstr "voulez-vous recevoir le Weekmail" -#: core/models.py:777 +#: core/models.py:816 msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:779 +#: core/models.py:818 msgid "get a notification for every click" msgstr "recevez une notification pour chaque click" -#: core/models.py:782 +#: core/models.py:821 msgid "get a notification for every refilling" msgstr "recevez une notification pour chaque rechargement" -#: core/models.py:805 +#: core/models.py:844 msgid "file name" msgstr "nom du fichier" -#: core/models.py:809 core/models.py:1157 +#: core/models.py:848 core/models.py:1196 msgid "parent" msgstr "parent" -#: core/models.py:823 +#: core/models.py:862 msgid "compressed file" msgstr "version allégée" -#: core/models.py:830 +#: core/models.py:869 msgid "thumbnail" msgstr "miniature" -#: core/models.py:838 core/models.py:855 +#: core/models.py:877 core/models.py:894 msgid "owner" msgstr "propriétaire" -#: core/models.py:842 core/models.py:1177 core/views/files.py:193 +#: core/models.py:881 core/models.py:1216 core/views/files.py:193 msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:845 core/models.py:1180 core/views/files.py:196 +#: core/models.py:884 core/models.py:1219 core/views/files.py:196 msgid "view group" msgstr "groupe de vue" -#: core/models.py:847 +#: core/models.py:886 msgid "is folder" msgstr "est un dossier" -#: core/models.py:848 +#: core/models.py:887 msgid "mime type" msgstr "type mime" -#: core/models.py:849 +#: core/models.py:888 msgid "size" msgstr "taille" -#: core/models.py:860 +#: core/models.py:899 msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:862 +#: core/models.py:901 msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:904 +#: core/models.py:943 msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:906 core/models.py:910 +#: core/models.py:945 core/models.py:949 msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:913 +#: core/models.py:952 msgid "You can not make a file be a children of a non folder file" msgstr "" "Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas " "un dossier" -#: core/models.py:924 +#: core/models.py:963 msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:941 +#: core/models.py:980 msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1081 +#: core/models.py:1120 msgid "Folder: " msgstr "Dossier : " -#: core/models.py:1083 +#: core/models.py:1122 msgid "File: " msgstr "Fichier : " -#: core/models.py:1140 +#: core/models.py:1179 msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1146 +#: core/models.py:1185 msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2319,55 +2319,55 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1164 +#: core/models.py:1203 msgid "page name" msgstr "nom de la page" -#: core/models.py:1172 +#: core/models.py:1211 msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1185 +#: core/models.py:1224 msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1192 +#: core/models.py:1231 msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1222 +#: core/models.py:1261 msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1225 +#: core/models.py:1264 msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1385 +#: core/models.py:1424 msgid "revision" msgstr "révision" -#: core/models.py:1386 +#: core/models.py:1425 msgid "page title" msgstr "titre de la page" -#: core/models.py:1387 +#: core/models.py:1426 msgid "page content" msgstr "contenu de la page" -#: core/models.py:1431 +#: core/models.py:1470 msgid "url" msgstr "url" -#: core/models.py:1432 +#: core/models.py:1471 msgid "param" msgstr "param" -#: core/models.py:1437 +#: core/models.py:1476 msgid "viewed" msgstr "vue" -#: core/models.py:1495 +#: core/models.py:1534 msgid "operation type" msgstr "type d'opération" @@ -2379,7 +2379,7 @@ msgstr "403, Non autorisé" msgid "404, Not Found" msgstr "404. Non trouvé" -#: core/templates/core/500.jinja:8 +#: core/templates/core/500.jinja:12 msgid "500, Server Error" msgstr "500, Erreur Serveur" @@ -2387,20 +2387,20 @@ msgstr "500, Erreur Serveur" msgid "Welcome!" msgstr "Bienvenue !" -#: core/templates/core/base.jinja:52 +#: core/templates/core/base.jinja:57 msgid "Username" msgstr "Nom d'utilisateur" -#: core/templates/core/base.jinja:54 +#: core/templates/core/base.jinja:59 msgid "Password" msgstr "Mot de passe" -#: core/templates/core/base.jinja:56 core/templates/core/login.jinja:4 +#: core/templates/core/base.jinja:61 core/templates/core/login.jinja:4 #: core/templates/core/password_reset_complete.jinja:5 msgid "Login" msgstr "Connexion" -#: core/templates/core/base.jinja:58 core/templates/core/register.jinja:18 +#: core/templates/core/base.jinja:63 core/templates/core/register.jinja:18 msgid "Register" msgstr "S'enregister" @@ -2463,9 +2463,9 @@ msgstr "Doceo" msgid "Positions" msgstr "Postes à pourvoir" -#: core/templates/core/base.jinja:182 -msgid "Calendar" -msgstr "Calendrier" +#: core/templates/core/base.jinja:182 core/templates/core/user_tools.jinja:118 +msgid "Elections" +msgstr "Élections" #: core/templates/core/base.jinja:183 msgid "Big event" @@ -2484,85 +2484,85 @@ msgstr "Forum" msgid "Gallery" msgstr "Photos" -#: core/templates/core/base.jinja:188 counter/models.py:224 +#: core/templates/core/base.jinja:189 counter/models.py:253 #: counter/templates/counter/counter_list.jinja:11 #: eboutic/templates/eboutic/eboutic_main.jinja:4 -#: eboutic/templates/eboutic/eboutic_main.jinja:24 +#: eboutic/templates/eboutic/eboutic_main.jinja:23 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:8 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 #: sith/settings.py:379 sith/settings.py:387 msgid "Eboutic" msgstr "Eboutic" -#: core/templates/core/base.jinja:190 +#: core/templates/core/base.jinja:192 msgid "Services" msgstr "Services" -#: core/templates/core/base.jinja:194 +#: core/templates/core/base.jinja:196 msgid "Matmatronch" msgstr "Matmatronch" -#: core/templates/core/base.jinja:195 launderette/models.py:47 +#: core/templates/core/base.jinja:197 launderette/models.py:47 #: launderette/templates/launderette/launderette_book.jinja:5 #: launderette/templates/launderette/launderette_book_choose.jinja:4 #: launderette/templates/launderette/launderette_main.jinja:4 msgid "Launderette" msgstr "Laverie" -#: core/templates/core/base.jinja:196 core/templates/core/file.jinja:20 +#: core/templates/core/base.jinja:198 core/templates/core/file.jinja:20 #: core/views/files.py:86 msgid "Files" msgstr "Fichiers" -#: core/templates/core/base.jinja:197 core/templates/core/user_tools.jinja:109 +#: core/templates/core/base.jinja:199 core/templates/core/user_tools.jinja:109 msgid "Pedagogy" msgstr "Pédagogie" -#: core/templates/core/base.jinja:201 +#: core/templates/core/base.jinja:203 msgid "My Benefits" msgstr "Mes Avantages" -#: core/templates/core/base.jinja:205 +#: core/templates/core/base.jinja:207 msgid "Sponsors" msgstr "Partenaires" -#: core/templates/core/base.jinja:206 +#: core/templates/core/base.jinja:208 msgid "Subscriber benefits" msgstr "Les avantages cotisants" -#: core/templates/core/base.jinja:210 +#: core/templates/core/base.jinja:212 msgid "Help" msgstr "Aide" -#: core/templates/core/base.jinja:214 +#: core/templates/core/base.jinja:216 msgid "FAQ" msgstr "FAQ" -#: core/templates/core/base.jinja:215 core/templates/core/base.jinja:257 +#: core/templates/core/base.jinja:217 core/templates/core/base.jinja:259 msgid "Contacts" msgstr "Contacts" -#: core/templates/core/base.jinja:216 +#: core/templates/core/base.jinja:218 msgid "Wiki" msgstr "Wiki" -#: core/templates/core/base.jinja:258 +#: core/templates/core/base.jinja:260 msgid "Legal notices" msgstr "Mentions légales" -#: core/templates/core/base.jinja:259 +#: core/templates/core/base.jinja:261 msgid "Intellectual property" msgstr "Propriété intellectuelle" -#: core/templates/core/base.jinja:260 +#: core/templates/core/base.jinja:262 msgid "Help & Documentation" msgstr "Aide & Documentation" -#: core/templates/core/base.jinja:261 +#: core/templates/core/base.jinja:263 msgid "R&D" msgstr "R&D" -#: core/templates/core/base.jinja:263 +#: core/templates/core/base.jinja:265 msgid "Site made by good people" msgstr "Site réalisé par des gens bons" @@ -3323,7 +3323,7 @@ msgstr "Photos de %(user_name)s" msgid "Download all my pictures" msgstr "Télécharger toutes mes photos" -#: core/templates/core/user_pictures.jinja:81 +#: core/templates/core/user_pictures.jinja:83 msgid "Error downloading your pictures" msgstr "Erreur de téléchargement de vos photos" @@ -3523,10 +3523,6 @@ msgstr "Créer UV" msgid "Moderate comments" msgstr "Modérer les commentaires" -#: core/templates/core/user_tools.jinja:118 -msgid "Elections" -msgstr "Élections" - #: core/templates/core/user_tools.jinja:120 msgid "See available elections" msgstr "Voir les élections disponibles" @@ -3717,8 +3713,8 @@ msgstr "Photos" msgid "User already has a profile picture" msgstr "L'utilisateur a déjà une photo de profil" -#: counter/app.py:31 counter/models.py:238 counter/models.py:647 -#: counter/models.py:677 launderette/models.py:41 stock/models.py:43 +#: counter/app.py:31 counter/models.py:267 counter/models.py:676 +#: counter/models.py:706 launderette/models.py:41 stock/models.py:43 msgid "counter" msgstr "comptoir" @@ -3726,137 +3722,137 @@ msgstr "comptoir" msgid "Ecocup regularization" msgstr "Régularization des ecocups" -#: counter/models.py:56 +#: counter/models.py:55 msgid "account id" msgstr "numéro de compte" -#: counter/models.py:58 +#: counter/models.py:57 msgid "recorded product" msgstr "produits consignés" -#: counter/models.py:61 +#: counter/models.py:60 msgid "customer" msgstr "client" -#: counter/models.py:62 +#: counter/models.py:61 msgid "customers" msgstr "clients" -#: counter/models.py:98 counter/views.py:377 +#: counter/models.py:107 counter/views.py:377 msgid "Not enough money" msgstr "Solde insuffisant" -#: counter/models.py:128 counter/models.py:155 +#: counter/models.py:137 counter/models.py:164 msgid "product type" msgstr "type du produit" -#: counter/models.py:161 +#: counter/models.py:170 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:162 +#: counter/models.py:171 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:163 +#: counter/models.py:172 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:165 +#: counter/models.py:174 msgid "icon" msgstr "icône" -#: counter/models.py:170 +#: counter/models.py:179 msgid "limit age" msgstr "âge limite" -#: counter/models.py:171 +#: counter/models.py:180 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:175 +#: counter/models.py:184 msgid "parent product" msgstr "produit parent" -#: counter/models.py:181 +#: counter/models.py:190 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:183 election/models.py:52 +#: counter/models.py:192 election/models.py:52 msgid "archived" msgstr "archivé" -#: counter/models.py:186 counter/models.py:772 +#: counter/models.py:195 counter/models.py:801 msgid "product" msgstr "produit" -#: counter/models.py:219 +#: counter/models.py:248 msgid "products" msgstr "produits" -#: counter/models.py:222 +#: counter/models.py:251 msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:224 +#: counter/models.py:253 msgid "Bar" msgstr "Bar" -#: counter/models.py:224 +#: counter/models.py:253 msgid "Office" msgstr "Bureau" -#: counter/models.py:227 +#: counter/models.py:256 msgid "sellers" msgstr "vendeurs" -#: counter/models.py:235 launderette/models.py:207 +#: counter/models.py:264 launderette/models.py:207 msgid "token" msgstr "jeton" -#: counter/models.py:390 +#: counter/models.py:419 msgid "bank" msgstr "banque" -#: counter/models.py:392 counter/models.py:482 +#: counter/models.py:421 counter/models.py:511 msgid "is validated" msgstr "est validé" -#: counter/models.py:395 +#: counter/models.py:424 msgid "refilling" msgstr "rechargement" -#: counter/models.py:459 eboutic/models.py:161 +#: counter/models.py:488 eboutic/models.py:223 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:460 counter/models.py:757 eboutic/models.py:162 +#: counter/models.py:489 counter/models.py:786 eboutic/models.py:224 msgid "quantity" msgstr "quantité" -#: counter/models.py:479 +#: counter/models.py:508 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:479 sith/settings.py:372 sith/settings.py:377 +#: counter/models.py:508 sith/settings.py:372 sith/settings.py:377 #: sith/settings.py:397 msgid "Credit card" msgstr "Carte bancaire" -#: counter/models.py:485 +#: counter/models.py:514 msgid "selling" msgstr "vente" -#: counter/models.py:512 +#: counter/models.py:541 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:513 +#: counter/models.py:542 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:515 counter/models.py:538 +#: counter/models.py:544 counter/models.py:567 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3868,59 +3864,59 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:652 +#: counter/models.py:681 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:655 +#: counter/models.py:684 msgid "permanency" msgstr "permanence" -#: counter/models.py:682 +#: counter/models.py:711 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:685 +#: counter/models.py:714 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:753 +#: counter/models.py:782 msgid "cash summary" msgstr "relevé" -#: counter/models.py:756 +#: counter/models.py:785 msgid "value" msgstr "valeur" -#: counter/models.py:758 +#: counter/models.py:787 msgid "check" msgstr "chèque" -#: counter/models.py:761 +#: counter/models.py:790 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:776 +#: counter/models.py:805 msgid "banner" msgstr "bannière" -#: counter/models.py:778 +#: counter/models.py:807 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:780 +#: counter/models.py:809 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:782 +#: counter/models.py:811 msgid "secret" msgstr "secret" -#: counter/models.py:838 +#: counter/models.py:867 msgid "uid" msgstr "uid" -#: counter/models.py:843 +#: counter/models.py:872 msgid "student cards" msgstr "cartes étudiante" @@ -4017,7 +4013,6 @@ msgid "Selling" msgstr "Vente" #: counter/templates/counter/counter_click.jinja:65 -#: eboutic/templates/eboutic/eboutic_main.jinja:27 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:11 msgid "Basket: " msgstr "Panier : " @@ -4323,84 +4318,120 @@ msgstr "Nombre de chèque" msgid "people(s)" msgstr "personne(s)" -#: eboutic/models.py:101 +#: eboutic/forms.py:105 +msgid "You have no basket." +msgstr "Vous n'avez pas de panier." + +#: eboutic/forms.py:110 +msgid "The request was badly formatted." +msgstr "La requête a été mal formatée." + +#: eboutic/forms.py:115 +msgid "The basket cookie was badly formatted." +msgstr "Le cookie du panier a été mal formaté." + +#: eboutic/forms.py:118 +msgid "Your basket is empty." +msgstr "Votre panier est vide" + +#: eboutic/forms.py:130 +#, python-format +msgid "%(name)s : this product does not exist." +msgstr "%(name)s : ce produit n'existe pas." + +#: eboutic/forms.py:139 +#, fuzzy, python-format +#| msgid "%(name)s : this product does not exist." +msgid "%(name)s : this product does not exist or may no longer be available." +msgstr "%(name)s : ce produit n'existe pas ou n'est peut-être plus disponnible." + +#: eboutic/forms.py:146 +#, fuzzy, python-format +#| msgid "You cannot buy %(nbr)d %(name)%s." +msgid "You cannot buy %(nbr)d %(name)%s." +msgstr "Vous ne pouvez pas acheter %(nbr)d %(name)s." + +#: eboutic/models.py:163 msgid "validated" msgstr "validé" -#: eboutic/models.py:114 +#: eboutic/models.py:173 msgid "Invoice already validated" msgstr "Facture déjà validée" -#: eboutic/models.py:158 +#: eboutic/models.py:220 msgid "product id" msgstr "ID du produit" -#: eboutic/models.py:159 +#: eboutic/models.py:221 msgid "product name" msgstr "nom du produit" -#: eboutic/models.py:160 +#: eboutic/models.py:222 msgid "product type id" msgstr "id du type du produit" -#: eboutic/models.py:177 +#: eboutic/models.py:239 msgid "basket" msgstr "panier" -#: eboutic/templates/eboutic/eboutic_main.jinja:35 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:32 -msgid "Basket amount: " -msgstr "Valeur du panier : " +#: eboutic/templates/eboutic/eboutic_main.jinja:32 +msgid "Your basket has been cleaned accordingly to those errors." +msgstr "Votre panier a été nettoyé en fonction de ces erreurs." #: eboutic/templates/eboutic/eboutic_main.jinja:39 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:36 msgid "Current account amount: " msgstr "Solde actuel : " -#: eboutic/templates/eboutic/eboutic_main.jinja:43 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:40 -msgid "Remaining account amount: " -msgstr "Solde restant : " +#: eboutic/templates/eboutic/eboutic_main.jinja:58 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:32 +msgid "Basket amount: " +msgstr "Valeur du panier : " -#: eboutic/templates/eboutic/eboutic_main.jinja:51 -msgid "Proceed to command" -msgstr "Procéder à la commande" +#: eboutic/templates/eboutic/eboutic_main.jinja:65 +msgid "Clear" +msgstr "Vider" + +#: eboutic/templates/eboutic/eboutic_main.jinja:71 +msgid "Validate" +msgstr "Valider" #: eboutic/templates/eboutic/eboutic_makecommand.jinja:4 msgid "Basket state" msgstr "État du panier" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:50 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:40 +msgid "Remaining account amount: " +msgstr "Solde restant : " + +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:51 msgid "Pay with credit card" msgstr "Payer avec une carte bancaire" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:55 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:56 msgid "" "AE account payment disabled because your basket contains refilling items." msgstr "" "Paiement par compte AE désactivé parce que votre panier contient des bons de " "rechargement." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:60 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:61 msgid "Pay with Sith account" msgstr "Payer avec un compte AE" #: eboutic/templates/eboutic/eboutic_payment_result.jinja:8 -msgid "Payment failed" -msgstr "Le paiement a échoué" - -#: eboutic/templates/eboutic/eboutic_payment_result.jinja:10 msgid "Payment successful" msgstr "Le paiement a été effectué" +#: eboutic/templates/eboutic/eboutic_payment_result.jinja:10 +msgid "Payment failed" +msgstr "Le paiement a échoué" + #: eboutic/templates/eboutic/eboutic_payment_result.jinja:12 msgid "Return to eboutic" msgstr "Retourner à l'eboutic" -#: eboutic/views.py:225 -msgid "You do not have enough money to buy the basket" -msgstr "Vous n'avez pas assez d'argent pour acheter le panier" - #: election/models.py:16 msgid "start candidature" msgstr "début des candidatures" @@ -4775,12 +4806,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:603 +#: sith/settings.py:609 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:603 +#: sith/settings.py:609 msgid "Drying" msgstr "Séchage" @@ -5440,204 +5471,204 @@ msgstr "Suppression de vente" msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:483 +#: sith/settings.py:489 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:484 +#: sith/settings.py:490 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:486 +#: sith/settings.py:492 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:490 +#: sith/settings.py:496 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:491 +#: sith/settings.py:497 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:492 +#: sith/settings.py:498 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:493 +#: sith/settings.py:499 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:494 +#: sith/settings.py:500 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:495 +#: sith/settings.py:501 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:496 +#: sith/settings.py:502 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:497 +#: sith/settings.py:503 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:499 +#: sith/settings.py:505 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:503 +#: sith/settings.py:509 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:504 +#: sith/settings.py:510 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:505 +#: sith/settings.py:511 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:507 +#: sith/settings.py:513 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:511 +#: sith/settings.py:517 msgid "One day" msgstr "Un jour" -#: sith/settings.py:512 +#: sith/settings.py:518 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:515 +#: sith/settings.py:521 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:520 +#: sith/settings.py:526 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:525 +#: sith/settings.py:531 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:530 +#: sith/settings.py:536 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:535 +#: sith/settings.py:541 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:541 +#: sith/settings.py:547 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:563 +#: sith/settings.py:569 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:564 +#: sith/settings.py:570 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:565 +#: sith/settings.py:571 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:566 +#: sith/settings.py:572 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:567 +#: sith/settings.py:573 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:568 +#: sith/settings.py:574 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:569 +#: sith/settings.py:575 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:570 +#: sith/settings.py:576 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:571 +#: sith/settings.py:577 msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:607 +#: sith/settings.py:613 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:608 +#: sith/settings.py:614 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:611 +#: sith/settings.py:617 msgid "A new pedagogy comment has been signaled for moderation" msgstr "" "Un nouveau commentaire de la pédagogie a été signalé pour la modération" -#: sith/settings.py:613 +#: sith/settings.py:619 #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:614 +#: sith/settings.py:620 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:615 +#: sith/settings.py:621 #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:616 +#: sith/settings.py:622 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:617 +#: sith/settings.py:623 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:618 +#: sith/settings.py:624 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:619 +#: sith/settings.py:625 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:631 +#: sith/settings.py:637 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:632 +#: sith/settings.py:638 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:633 +#: sith/settings.py:639 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:634 +#: sith/settings.py:640 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:635 +#: sith/settings.py:641 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:643 +#: sith/settings.py:649 msgid "AE tee-shirt" msgstr "Tee-shirt AE" @@ -5880,11 +5911,11 @@ msgid "Eboutic is reserved to specific users. In doubt, don't use it." msgstr "" "Eboutic est réservé à des cas particuliers. Dans le doute, ne l'utilisez pas." -#: subscription/views.py:96 +#: subscription/views.py:104 msgid "A user with that email address already exists" msgstr "Un utilisateur avec cette adresse email existe déjà" -#: subscription/views.py:119 +#: subscription/views.py:127 msgid "You must either choose an existing user or create a new one properly" msgstr "" "Vous devez soit choisir un utilisateur existant, soit en créer un proprement" @@ -6189,8 +6220,6 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée." msgid "Maximum characters: %(max_length)s" msgstr "Nombre de caractères max: %(max_length)s" -#~ msgid "Indifferent" -#~ msgstr "Indifferent" - -#~ msgid "Sex" -#~ msgstr "Sexe" +#: eboutic/templates/eboutic/eboutic_main.jinja +msgid "There are no items available for sale" +msgstr "Aucun article n'est disponible à la vente" diff --git a/sith/settings.py b/sith/settings.py index 74331540..7418a5fc 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -703,4 +703,5 @@ SITH_FRONT_DEP_VERSIONS = { "https://github.com/getsentry/sentry-javascript/": "4.0.6", "https://github.com/jhuckaby/webcamjs/": "1.0.0", "https://github.com/vuejs/vue-next": "3.2.18", + "https://github.com/alpinejs/alpine": "3.10.3", }