Compare commits

..

222 commits

Author SHA1 Message Date
Aurélien Delobelle 5aa2d3cbac kfet.kpsul -- Make the low-stock indicator appear…
…if ordered quantity is greater than the current stock.

Also fix the focus after a reset of the selected account.
2018-01-13 02:04:42 +01:00
Aurélien Delobelle 367b5899fc Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2018-01-12 18:49:13 +01:00
Aurélien Delobelle 5ef219c88e kfet -- Update tests to reflect last changes + fix
- Remove duplicate categories returned by the view 'kpsul_articles_data'.
- Fix in account filtering of the view 'history_json'.

- Tests of the view 'kpsul_articles_data' are updated to reflect changes of the
  data format.
- The views returning Account and Checkout data as JSON no longer
  exists. The related tests are moved.
- The view canceling transfers has been merged with the one canceling
  the other operations. The related tests are moved.
2018-01-12 18:48:42 +01:00
Ludovic Stephan f93cadc12c Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2018-01-10 19:12:09 +01:00
Ludovic Stephan f03ce35126 Merge branch 'master' into aureplop/kpsul_js_refactor 2018-01-10 19:09:41 +01:00
Ludovic Stephan b62f0293dd Merge branch 'master' into aureplop/kpsul_js_refactor 2018-01-10 18:55:33 +01:00
Aurélien Delobelle ba1eabe240 "Fix" inconsistents amounts/balances 2017-05-31 21:29:53 +02:00
Aurélien Delobelle 5ab7519c8d ForestDisplay update node.
- ForestDisplay update method update the container without re-rendering
from scratch the object. This allows persistance of properties set by
other components.
- Fix unselect on basket updates from keyboard.
2017-05-31 17:14:31 +02:00
Aurélien Delobelle eff1b7ff19 Cleaning - Article autocomplete, ForestDisplay and more
K-Psul
- Improve article autocompletion.

ArticleManager
- "selected" property becomes a reference to an article in the data properties.

ForestDisplay
- Add data property "object" linked to object being represented
- Use class to identify objects instead of id. Allow multiple displays of same ModelForest.
- New get_class method returns the class selector to find an object container in the DOM.
- New get_dom method returns the DOM element from an object in the ModelForest.

Cancellation view
- Fix 500 on cancel with already canceled opes/transfers
2017-05-21 20:03:37 +02:00
Aurélien Delobelle 51083f9195 Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2017-05-21 13:01:51 +02:00
Ludovic Stephan c76b729320 Merge branch 'aureplop/ws_checkout' into 'aureplop/kpsul_js_refactor'
Add last statement update support.

See merge request !232
2017-05-19 21:50:29 +02:00
Aurélien Delobelle 8a9d6712ee Merge branch 'aureplop/kpsul_js_refactor' into aureplop/js_basket 2017-05-18 13:41:56 +02:00
Aurélien Delobelle 914d2ca870 Add last statement update support.
K-Psul
- Add handler for checkout data (balance update)
- New statements are sent through ws, and display live on K-Psul.
2017-05-18 13:33:53 +02:00
Aurélien Delobelle 86a3ae3c2f clean
- basket.add_withdraw takes a positive amount
- move models and formatters definitions with their siblings
2017-05-18 12:49:39 +02:00
Ludovic Stephan ef1f00c15b Merge branch 'aureplop/fix_ws' into 'aureplop/kpsul_js_refactor'
Fix some ws updates on khistory

See merge request !230
2017-05-18 03:39:53 +02:00
Aurélien Delobelle a0503d0c53 Refactor K-Psul basket (big part)
K-Psul - Basket refactor
- Almost done.

ModelForest
- Add create method (based on previous get_or_create). Direction defaults to 0.
- Add delete method.
- Methods find, traverse, update, delete can also take a model (class) as first
  argument. String representation of model still works.
- Fix child linking to parent in create method.

ModelForest -> ForestDisplay
- (One-way data binding) Changes on a ModelForest are directly reflected on
  listening ForestDisplay(s).
- ArticleManager and KHistory become simpler.

Config
- Add addcost key, shorthand for double addcost keys check.

K-Psul
- Improve display for basket summary and previous operation.
- Clean js code / duplicates.
- Some components gains chance to trigger/handle events. They are really happy.
  Eg basket amounts and summary are updated thanks to these events if the
  selected account is changed.

Formatters
- Fixes addcost and amount display.

History
- Fix options management (api_options were overrided and K-Psul displayed more
  than the last day history).
- Fix data display, thanks to formatters fixes and modelforest fixes.
2017-05-18 02:13:42 +02:00
Aurélien Delobelle 24b9aaae39 Revert template literals use
They were adding spaces at some places.
2017-05-16 18:47:26 +02:00
Aurélien Delobelle cb0c0be8a2 filters on ws for khistory
- fix filters on ws khistory updates
- add filters for transfers against accounts
2017-05-16 18:08:40 +02:00
Aurélien Delobelle 02a015e633 fix data sent to ws by perform_transfers 2017-05-16 17:08:51 +02:00
Aurélien Delobelle bacc079778 Merge branch 'master' into aureplop/kpsul_js_refactor 2017-05-16 16:48:12 +02:00
Aurélien Delobelle bc71e1628a keep eslint quiet 2017-05-16 12:27:39 +02:00
Aurélien Delobelle 311e0c48bd Merge branch 'Aufinal/refactor_history' into 'aureplop/kpsul_js_refactor'
Aufinal/refactor history

See merge request !192
2017-05-16 10:48:07 +02:00
Ludovic Stephan 6a6fc38ead Add selection reset to cancel_opes 2017-05-15 21:10:39 -03:00
Ludovic Stephan 43e772363e Extend history options 2017-05-15 19:29:12 -03:00
Ludovic Stephan 93c8844b3f typo 2017-05-15 19:28:24 -03:00
Ludovic Stephan c2da055b60 Remove duplicate ws updates 2017-05-15 18:59:18 -03:00
Ludovic Stephan ac33e6302e Fix addExistingPurchase + few other bugs 2017-05-15 17:39:33 -03:00
Ludovic Stephan ad42687293 Fix tranfers page 2017-05-15 15:27:46 -03:00
Ludovic Stephan f4cb1e2e83 Add opesonly option 2017-05-15 14:17:58 -03:00
Ludovic Stephan c12c705f8b Bind ForestDisplay to initial data 2017-05-14 20:18:31 -03:00
Ludovic Stephan 31b742fdb7 Move ws update to respective classes 2017-05-14 17:19:09 -03:00
Ludovic Stephan 8b8a3f8a25 Update button in history 2017-04-24 14:46:16 -03:00
Ludovic Stephan 16dbfed977 Add chidren div in display 2017-04-24 14:12:03 -03:00
Ludovic Stephan 5096e5f129 Adapt ArticleManager 2017-04-24 13:25:18 -03:00
Ludovic Stephan 46ac82fd27 Adapt history to changes 2017-04-24 13:25:07 -03:00
Ludovic Stephan e283439ebc Create ForestDisplay class
- Store templates, container and data
- All display functions removed from ModelForest
2017-04-24 13:21:56 -03:00
Ludovic Stephan 20d635137c Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_history 2017-04-24 11:30:01 -03:00
Ludovic Stephan b2a5dfd682 Move permission check 2017-04-14 12:51:58 -03:00
Ludovic Stephan 100686457b Change select_related for future compatibility 2017-04-14 12:47:15 -03:00
Ludovic Stephan b544d6c5b3 Better alignment 2017-04-14 12:45:38 -03:00
Ludovic Stephan 659b20891e Add filter to cancel_opes 2017-04-14 12:43:59 -03:00
Ludovic Stephan 034a661444 Tweaks on kfet.js 2017-04-14 12:12:43 -03:00
Ludovic Stephan de865c61aa Move container clearing 2017-04-14 11:36:52 -03:00
Aurélien Delobelle b0d35667b3 Merge branch 'master' into aureplop/kpsul_js_refactor 2017-04-14 15:56:44 +02:00
Ludovic Stephan 8c02e5da0c Trigger event on history itself 2017-04-10 15:40:29 -03:00
Ludovic Stephan 2eba6892a2 Revert "Change event triggered when canceling opes"
This reverts commit ce3d8aa6f7.
2017-04-10 15:34:57 -03:00
Ludovic Stephan ce3d8aa6f7 Change event triggered when canceling opes 2017-04-10 15:08:17 -03:00
Ludovic Stephan 84d478271b Compatibility changes on history.js 2017-04-10 12:34:43 -03:00
Ludovic Stephan cd0e4c6f3e Allow (basic) chaining on api_with_auth 2017-04-10 12:33:24 -03:00
Ludovic Stephan 983a55780f Add cancel_history event 2017-04-10 12:32:35 -03:00
Ludovic Stephan cfb39b1050 Better default options 2017-04-10 11:30:00 -03:00
Aurélien Delobelle c75b2946e3 Merge branch 'master' into aureplop/kpsul_js_refactor 2017-04-10 13:08:23 +02:00
Ludovic Stephan 47da80f21c Add related objects 2017-04-09 23:24:50 -03:00
Ludovic Stephan 5e8752632c Add index for Day objects 2017-04-09 21:50:47 -03:00
Ludovic Stephan 688d5bba29 Adapt history to new structure 2017-04-09 17:54:37 -03:00
Ludovic Stephan 051231a031 Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_history 2017-04-09 16:47:05 -03:00
Aurélien Delobelle 7fc07ac603 Merge branch 'Aufinal/clean_modelforest' into 'aureplop/kpsul_js_refactor'
Change ModelForest inner structure
- Intermediary model for nodes removed
- More descriptive ModelForest structure in constructor.structure, child_sort not defined in Python function anymore.
- More intuitive access to parent and children of nodes (e.g. article.category, category.articles...)

See merge request !215
2017-04-09 21:28:16 +02:00
Ludovic Stephan 323f019c0d Check if children is non empty 2017-04-09 13:35:01 -03:00
Ludovic Stephan 9ad208a171 Change child sort + bugfix grom prev commit 2017-04-09 12:30:15 -03:00
Ludovic Stephan 73fb3c419e Add stop check in traverse 2017-04-09 11:43:51 -03:00
Ludovic Stephan 9ba13a81ee Adapt add_to_container + small improvements 2017-04-06 00:10:39 -03:00
Ludovic Stephan 23d19545a7 Add back root_sort 2017-04-05 22:23:56 -03:00
Ludovic Stephan df47bedae1 Change ModelForest inner structure 2017-04-05 22:10:21 -03:00
Aurélien Delobelle e4dd434608 no longer use model_to_dict
- fix cof status on k-psul
2017-04-05 23:33:45 +02:00
Aurélien Delobelle e4ccd88dfd Merge branch 'master' into aureplop/kpsul_js_refactor 2017-04-05 23:18:33 +02:00
Ludovic Stephan ed0a82ed5d Add no_trigramme option 2017-04-05 16:51:17 -03:00
Aurélien Delobelle 6a8f41849b Merge branch 'Aufinal/refactor_articles' into 'aureplop/kpsul_js_refactor'
Aufinal/refactor articles

See merge request !173
2017-04-05 17:56:07 +02:00
Ludovic Stephan 5c422e892a Add children fo traverse callback 2017-04-05 12:31:19 -03:00
Ludovic Stephan f57c292184 Rename history var 2017-04-05 12:13:24 -03:00
Ludovic Stephan 508e7ec23f Change traverse and find behavior 2017-04-05 12:00:39 -03:00
Ludovic Stephan 88f7ea941d Move selection logic to another class 2017-04-05 11:26:33 -03:00
Ludovic Stephan 8d13c0a4bb Add fetch method 2017-04-05 10:59:59 -03:00
Ludovic Stephan 290d4ecb6e Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-04-05 10:03:30 -03:00
Ludovic Stephan 7ec7ed2696 Rename History class 2017-04-05 09:53:12 -03:00
Ludovic Stephan e051631a34 Use WebSocket classes 2017-04-05 09:28:32 -03:00
Ludovic Stephan 360c442a4e Remove useless class 2017-04-05 09:24:27 -03:00
Ludovic Stephan 1761c5f1bd Change fromAPI logic 2017-04-05 09:13:00 -03:00
Ludovic Stephan 840010b63f Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_articles 2017-04-05 09:03:59 -03:00
Ludovic Stephan a29de134f1 Move focus ; move is_low_stock to method 2017-04-05 08:58:46 -03:00
Aurélien Delobelle 2e0de75471 kpsul - fix account balance ukf 2017-04-05 13:18:01 +02:00
Aurélien Delobelle 6bb9280b0d Merge branch 'aureplop/clean_js' into 'aureplop/kpsul_js_refactor'
Enhance APIModelObject

See merge request !213
2017-04-05 04:30:34 +02:00
Aurélien Delobelle 6be6202b3f few cleans 2017-04-05 04:26:50 +02:00
Aurélien Delobelle f1aaad7317 Better jquery ajax calls management
It becomes the same as the original jQuery ajax object.
For example, callbacks can be queued.
get_by_apipk and from_API of ModelObject returns the ajax object.

Example (js):
Account.get_by_apipk('AAA')
    .done(function (data) {
        console.log(data)
    })
    .fail( () => console.log('cool') )
    ...
    .done( ...
2017-04-05 04:05:31 +02:00
Aurélien Delobelle efbcde163b clean some js
- clean buttons code on account and checkout
- merge CheckoutRead and kpsul_checkout_data views (the first won)

APIModelObject interface
- add url_create, url_update, url_update_for
- rename url_object_for and url_object to url_read_for and url_read
2017-04-05 03:43:51 +02:00
Ludovic Stephan 3b9affb3f3 Add focus methods 2017-04-04 20:18:53 -03:00
Ludovic Stephan 9c559d9ec3 Add articles reset to kpsul.reset 2017-04-04 19:54:03 -03:00
Ludovic Stephan 021937a38e Small bugfixes 2017-04-04 19:52:12 -03:00
Ludovic Stephan cb28b928c4 Remove articleSelect from _env 2017-04-04 19:42:30 -03:00
Ludovic Stephan e5791efe4d Remove last traces of old articles 2017-04-04 19:41:15 -03:00
Ludovic Stephan 05156f37c6 Update addExistingPurchase 2017-04-04 19:34:22 -03:00
Ludovic Stephan 514f1da6df Fix SpecialOpeFormatter 2017-04-03 21:26:33 -03:00
Ludovic Stephan ec9f47274a Fix WS update functions 2017-04-03 21:24:07 -03:00
Ludovic Stephan 20b7156e1f Improve type display 2017-04-03 21:18:07 -03:00
Ludovic Stephan a173be4f7d Use api_with_auth in history 2017-04-03 21:14:06 -03:00
Ludovic Stephan 07290f6fec Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-04-03 21:00:12 -03:00
Ludovic Stephan 3ce4dc5c85 Add article stock management 2017-04-03 20:14:45 -03:00
Ludovic Stephan b91edc9c7d Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_articles 2017-04-03 20:03:58 -03:00
Aurélien Delobelle 9d2298a089 Merge branch 'master' into aureplop/kpsul_js_refactor
Not chill.
2017-04-04 00:51:49 +02:00
Aurélien Delobelle dcda67aaf7 Merge branch 'Aufinal/dialog_utils' into 'aureplop/kpsul_js_refactor'
Utilitaires de dialogue

Ajoute deux type de dialogue avec l'utilisateur
- une classe UserDialog pour ouvrir un simple dialogue jconfirm
- une fonction api_with_auth pour gérer toutes les requêtes API pouvant 
nécessiter un mot de passe

See merge request !199
2017-04-03 23:25:55 +02:00
Ludovic Stephan 5020037103 api_lock inside kfet.js 2017-04-03 16:08:40 -03:00
Ludovic Stephan 4af2562121 More clarity in argument names 2017-04-01 09:34:02 -03:00
Ludovic Stephan 29836fd15c Remove deprecated option 2017-04-01 09:24:18 -03:00
Ludovic Stephan 2774dbb5de Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-31 23:51:13 -03:00
Ludovic Stephan 7d93d91af9 Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_articles 2017-03-31 23:50:33 -03:00
Ludovic Stephan 236dcb4644 Tweaks to UserDialog 2017-03-31 18:10:06 -03:00
Ludovic Stephan 582cdebaa1 Better callback management 2017-03-31 17:49:22 -03:00
Ludovic Stephan 08c752f1b3 Simplify addcost management 2017-03-26 18:10:26 -03:00
Ludovic Stephan 5101400f64 Use callback_as_dict for addcost 2017-03-26 17:57:51 -03:00
Ludovic Stephan c1f70d9d0a Add capslock support for inputs 2017-03-26 17:51:55 -03:00
Ludovic Stephan 6d92df4155 Merge branch 'aureplop/kpsul_js_refactor' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/dialog_utils 2017-03-26 15:21:33 -03:00
Aurélien Delobelle 842f2cecc1 fix import, fix ope with addcost enabled, move Config location 2017-03-26 15:12:03 +02:00
Aurélien Delobelle bc6ecda0c8 fix addcost kspul 2017-03-26 14:59:21 +02:00
Aurélien Delobelle 24f72ae7d4 add missing ; 2017-03-26 14:52:23 +02:00
Aurélien Delobelle b81b33c056 allow chaining on container in display method 2017-03-26 14:51:16 +02:00
Aurélien Delobelle 485ae86a42 add update method to ModelObject 2017-03-26 14:46:46 +02:00
Ludovic Stephan 3bf7a066a2 Adapt history.html to new functions 2017-03-26 00:33:33 -03:00
Ludovic Stephan 3f07bf56fa Remove console log 2017-03-26 00:25:51 -03:00
Ludovic Stephan dbcfc6df46 Remove duplicate kfet.js import 2017-03-26 00:21:17 -03:00
Ludovic Stephan 87943ea7b9 Adapt kpsul to new functions 2017-03-26 00:20:47 -03:00
Ludovic Stephan 6be2f086df Add generic functions for confirm dialogs 2017-03-26 00:20:28 -03:00
Ludovic Stephan a1c976185c Fix transfer sort 2017-03-25 13:44:35 -03:00
Ludovic Stephan e6735d44ba Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-25 12:42:26 -03:00
Ludovic Stephan 0997d85083 Merge branch 'aureplop/kpsul_js_refactor' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/refactor_history 2017-03-25 12:39:30 -03:00
Ludovic Stephan 5ff8f69bfa Merge branch 'aureplop/kpsul_js_refactor' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/refactor_articles 2017-03-25 12:35:04 -03:00
Aurélien Delobelle 6afbcb44a1 delete array comprehesion 2017-03-25 10:26:45 +01:00
Aurélien Delobelle 967748ded3 details link to read instead of update
- It was the old behaviour.
2017-03-25 09:58:20 +01:00
Aurélien Delobelle 72970c6be7 Fix api call on new transfers
- Use django-js-reverse
- Replace old url (deleted) with standard url
2017-03-25 09:44:55 +01:00
Ludovic Stephan aa6a50a6e7 Simplify JS-Python interface for cancel_ops 2017-03-24 23:57:27 -03:00
Aurélien Delobelle ac18cbd9d9 fix by on last statement 2017-03-24 22:10:34 +01:00
Aurélien Delobelle 0d02d47d33 move utils functions 2017-03-24 22:05:04 +01:00
Aurélien Delobelle 2e3bd5bd7a fix search box 2017-03-24 21:41:34 +01:00
Aurélien Delobelle abce961d91 use django-js-reverse 2017-03-24 21:28:33 +01:00
Aurélien Delobelle a9d1a6aae9 clean array iteration 2017-03-24 21:24:06 +01:00
Ludovic Stephan 1fcd53d780 Continue renaming node.type to node.modelname 2017-03-20 00:42:12 -03:00
Ludovic Stephan fe965875f7 Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-20 00:33:30 -03:00
Ludovic Stephan 3465dd7045 Change node.type to node.modelname for clarity 2017-03-20 00:26:11 -03:00
Ludovic Stephan c99e4f26d0 Move history initialisation as Config.reset callback 2017-03-19 04:59:55 +01:00
Ludovic Stephan 66c5a6953c Improve websocket filter for special history pages 2017-03-19 04:29:54 +01:00
Ludovic Stephan 7a00096170 Add support for account_read history 2017-03-18 22:48:50 -03:00
Ludovic Stephan 951932a6c8 Add support and websocket to transfers.html 2017-03-18 22:35:47 -03:00
Ludovic Stephan fa64a68378 Add strict mode to history.html script 2017-03-18 22:15:09 -03:00
Ludovic Stephan fc3e86aea6 Add websocket support to ArticleManager 2017-03-18 22:06:30 -03:00
Ludovic Stephan b8a307b4a6 Add support for kfet/history page 2017-03-18 14:05:11 -03:00
Ludovic Stephan 1d532616b7 Fix bugs introduced by previous commit 2017-03-18 02:47:30 -03:00
Ludovic Stephan 9e905b0f8b Remove kpsul dependence from history (oops) 2017-03-18 02:40:34 -03:00
Ludovic Stephan 644b08973a Add websocket support for history 2017-03-18 02:32:27 -03:00
Ludovic Stephan 34bb680570 Add History equivalent functions 2017-03-18 02:31:52 -03:00
Ludovic Stephan b655907bd4 Add history to KPsul manager 2017-03-18 02:29:02 -03:00
Ludovic Stephan 14b922634d Remove deprecated history functions 2017-03-18 02:28:01 -03:00
Ludovic Stephan 5c9c206f68 Adapt css to new canceled syntax 2017-03-18 02:25:44 -03:00
Ludovic Stephan b0b1fdf936 Add jsdoc comments to history models 2017-03-17 21:00:58 -03:00
Ludovic Stephan 565a054323 Add support for low stock css 2017-03-17 17:30:22 -03:00
Ludovic Stephan 58c57c6f89 Add hierarchy of needed models, w/ formatters 2017-03-17 16:00:49 -03:00
Ludovic Stephan 47fe74fbb0 template specification 2017-03-17 15:59:52 -03:00
Ludovic Stephan df0ea96b41 Adapt history_json return value to ModelForest standards 2017-03-17 15:59:21 -03:00
Ludovic Stephan 53f89f53e0 Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-17 12:34:48 -03:00
Ludovic Stephan a7de396aa3 Better comparison control 2017-03-17 12:33:43 -03:00
Ludovic Stephan 11603cee69 Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history 2017-03-17 00:19:45 -03:00
Ludovic Stephan 9ab2a11432 Finish adapting ArticleManager and Autocomplete 2017-03-16 22:26:59 -03:00
Ludovic Stephan 91f14deda1 last tweaks and doc 2017-03-16 22:26:30 -03:00
Ludovic Stephan 1c5ac561a3 Change article table into divs 2017-03-16 22:26:08 -03:00
Ludovic Stephan f0a80561ed Add article display to Config callback 2017-03-16 22:24:57 -03:00
Ludovic Stephan 01295d464d Adapt ArticleAutocomplete to new format 2017-03-16 01:22:46 -03:00
Ludovic Stephan 2ce96bce1b Add traverse function to ModelTree 2017-03-16 01:21:50 -03:00
Ludovic Stephan 770c185bd0 Modify sort in ModelTree 2017-03-16 01:21:18 -03:00
Ludovic Stephan 3d76079439 Add correct syntax to category data 2017-03-16 01:20:06 -03:00
Ludovic Stephan 08d1521d81 Adapt ArticleManagerto new API 2017-03-15 22:40:06 -03:00
Ludovic Stephan fe6823fc7b Adapt article_data return value to ModelTree standards 2017-03-15 22:39:30 -03:00
Ludovic Stephan 1570d9f494 Polish ModelForest class 2017-03-15 22:10:56 -03:00
Ludovic Stephan 0219d998ac model tree struct draft 2017-03-15 02:45:13 -03:00
Ludovic Stephan 66beeb5bd0 transfer history added 2017-03-11 01:41:21 -03:00
Ludovic Stephan ab6b0d52f2 Merge branch 'Aufinal/transferts_historique' into Aufinal/refactor_history 2017-03-11 00:45:54 -03:00
Ludovic Stephan ac2e773f9e opelist class 2017-03-10 22:09:23 -03:00
Ludovic Stephan 741bac880b dummy History test class 2017-03-10 22:09:02 -03:00
Ludovic Stephan 8eae3cee7f adapt history view 2017-03-10 22:08:44 -03:00
Ludovic Stephan 02485afd9b doc and compare function 2017-03-10 19:59:15 -03:00
Ludovic Stephan eac6c42041 day, opegroup and ope model drafts 2017-03-10 19:57:36 -03:00
Ludovic Stephan e1abff2242 last tweaks 2017-03-09 22:31:59 -03:00
Ludovic Stephan 8aa4fa2dce modify article API return 2017-03-09 09:21:07 -03:00
Ludovic Stephan c9b7683238 articlelist and modellist finished 2017-03-09 09:20:53 -03:00
Ludovic Stephan fe8e5d7e46 move and adapt manager and completion 2017-03-09 09:20:23 -03:00
Ludovic Stephan a05a075962 apimodellist & articlelist 2017-03-08 10:47:51 -03:00
Ludovic Stephan d5dfd5fa93 move ModelList def 2017-03-07 18:36:56 -03:00
Ludovic Stephan c9cce5b125 remove temp file 2017-03-07 17:58:10 -03:00
Ludovic Stephan 2cc0e0cffe modellist suite et fin 2017-03-07 17:57:40 -03:00
Ludovic Stephan db9c14f768 articlelist wip 2017-03-06 02:43:48 -03:00
Ludovic Stephan 643503269e add Article and Category models 2017-03-02 06:50:47 -03:00
Aurélien Delobelle a9cb50b38d Better k-fet js and more
JavaScript
----------
- Basic classes that can be inherited to define a new class for a
  django model in javascript.
- Formatters classes are used to render properties and attributes of
  the instances of models classes.
- New classes to handle Account, Checkout, Statement models.
- Refactor K-Psul JS (part n/m).
- Better file organization.

Views
-----
- 'kpsul.checkout_data' is cleaner. Last statement is added to the JSON
  response with GET paramater 'last_statement'.
- 'account.read.json' is merged in account.read. JSON response is sent if
  GET parametter 'format' is set to 'json'.
- Fix PEP8 of concerned views.

New requirement: django-js-reverse
----------------------------------
Used to resolve the URLs defined in the project in JavaScript.
See https://github.com/ierror/django-js-reverse
2017-02-23 22:07:38 +01:00
Aurélien Delobelle ee848d210e refactor account k-psul js - part 2/?
- les événements de AccountSearch sont enregistrés depuis ces classes (plus depuis le manager)
- ajout d'une classe AccountSelection s'occupant de la sélection d'un
  compte par l'utilisateur
- la méthode update de AccountManager peut maintenant prendre un
  trigramme et le set correctement, à défaut elle récupère le trigramme
  via AccountSelection
2017-02-15 14:09:20 +01:00
Aurélien Delobelle fdcf4c3ab0 fix property location 2017-02-13 13:48:57 +01:00
Aurélien Delobelle d9fc683525 update account_data[] to account_manager.account. 2017-02-13 00:41:41 +01:00
Aurélien Delobelle 890be9b343 refactor account k-psul js - part 1
- nouvelle classe - Account: stocke, sert et récupère les données
  associées à un compte
- nouvelle classe - AccountManager: interface pour le management de la
  partie Account de K-Psul
- nouvelle classe - AccountSearch: module de recherche d'un compte
- nouvelles classes - AccountFormatter, StandardAccountFormatter,
  LIQAccountFormatter: styles de formattage des données d'un compte

- désactive l'autocomplétion dans la recherche d'un compte
- fix #89: "Entrée" dans le champ de trigramme met le compte LIQ
2017-02-13 00:23:32 +01:00
Aurélien Delobelle 5c7a1d6874 Refactor JS Settings K-Psul
- change name: Settings -> Config
- provide interface `Config` to get/set parameters
- `Config` uses global object `window.config` to store key/value
- `Config` setters handle types
2017-02-12 13:26:02 +01:00
Aurélien Delobelle 8279bddf4e clean js k-psul
- K-Psul JavaScript uses strict-mode (when JS try to do better things,
  we should follow)
2017-02-12 06:10:17 +01:00
Ludovic Stephan 481409253b simpler string pluralizing 2017-02-11 21:44:32 -02:00
Ludovic Stephan 02735642f1 better pluralize 2017-02-11 21:41:09 -02:00
Ludovic Stephan fc2de20ab8 use switch + move text 2017-02-11 21:38:07 -02:00
Ludovic Stephan db94a8904c remove addition of new opegroups 2017-02-07 18:45:57 -02:00
Ludovic Stephan b404c989ff pep8 2017-02-05 03:42:02 -02:00
Ludovic Stephan 1d5e693045 ws for transfers 2017-02-05 03:26:15 -02:00
Ludovic Stephan 1ea334341b ws update for history and transfer pages 2017-02-05 03:04:41 -02:00
Ludovic Stephan f8aa67721c fix socket update 2017-02-05 02:19:20 -02:00
Ludovic Stephan 8895daff6a Merge branch 'Aufinal/transferts_historique' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2017-02-04 23:22:54 -02:00
Ludovic Stephan f06a732da5 remove unnecessary function 2016-12-11 23:29:05 -02:00
Ludovic Stephan e52c44580f pluralize function 2016-12-11 23:23:12 -02:00
Ludovic Stephan ee54b36696 minor imprevements to history 2016-12-11 23:22:59 -02:00
Ludovic Stephan a9e1cd01db add transfersonly option 2016-12-11 23:22:14 -02:00
Ludovic Stephan 3f35dc2c06 unite transfer history 2016-12-11 23:21:36 -02:00
Ludovic Stephan 1dbbad38b9 transfer cancellation html 2016-12-11 21:00:42 -02:00
Ludovic Stephan 66304359c0 unite cancel_ope and cancel_transfer 2016-12-11 16:22:55 -02:00
Ludovic Stephan 49bef61e53 filter transfers frop opes 2016-12-11 14:45:52 -02:00
Ludovic Stephan 2c2da60e54 send data for cancel 2016-12-10 23:52:26 -02:00
Ludovic Stephan 0b61a48c65 fix selection 2016-12-10 23:13:43 -02:00
Ludovic Stephan 85af7fe485 filter on id 2016-12-09 17:29:40 -02:00
Ludovic Stephan b3b49d5768 Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2016-12-09 16:48:18 -02:00
Ludovic Stephan b0a21119fa Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2016-12-09 01:25:40 -02:00
Ludovic Stephan 36edc334d4 add transfer information 2016-12-09 00:26:25 -02:00
Ludovic Stephan ac0356386a add css for transfers 2016-12-09 00:26:07 -02:00
Ludovic Stephan 027bc2e94b Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique 2016-12-04 00:22:46 -02:00
Ludovic Stephan f747c0c389 print transfers (BROKEN) 2016-12-02 00:17:03 -02:00
Ludovic Stephan 5a354c61a0 fetch transfers as well 2016-12-02 00:16:40 -02:00
Ludovic Stephan 971848cb1b database lookups 2016-12-01 01:29:28 -02:00
755 changed files with 58594 additions and 95156 deletions

1
.envrc
View file

@ -1 +0,0 @@
use nix

7
.gitignore vendored
View file

@ -5,19 +5,12 @@ cof/settings.py
settings.py
*~
venv/
.venv/
.vagrant
/src
media/
*.log
.sass-cache/
*.sqlite3
.coverage
# PyCharm
.idea
.cache
# VSCode
.vscode/
.direnv

View file

@ -1,13 +1,16 @@
image: "python:3.7"
services:
- postgres:latest
- redis:latest
variables:
# GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
DBHOST: "postgres"
REDIS_HOST: "redis"
REDIS_PASSWD: "dummy"
# Cached packages
PIP_CACHE_DIR: "$CI_PROJECT_DIR/vendor/pip"
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
# postgres service configuration
POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
@ -17,79 +20,22 @@ variables:
# psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD
# apps to check migrations for
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
cache:
paths:
- vendor/python
- vendor/pip
- vendor/apt
.test_template:
before_script:
- mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade -r requirements-prod.txt coverage tblib
- python --version
after_script:
- coverage report
services:
- postgres:11.7
- redis:latest
cache:
key: test
paths:
- vendor/
# For GitLab CI to get coverage from build.
# Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
before_script:
- mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
coftest:
test:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
bdstest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod"
script:
- coverage run manage.py test bds clubs events --parallel
linters:
stage: test
before_script:
- mkdir -p vendor/pip
- pip install --upgrade black isort flake8
script:
- black --check .
- isort --check --diff .
# Print errors only
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared
cache:
key: linters
paths:
- vendor/
# Check whether there are some missing migrations.
migration_checks:
stage: test
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.local"
before_script:
- mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py
- pip install --upgrade -r requirements-devel.txt
- python --version
script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS
services:
# this should not be necessary…
- postgres:11.7
cache:
key: migration_checks
paths:
- vendor/
- python manage.py test

View file

@ -1,106 +0,0 @@
#!/usr/bin/env bash
# pre-commit hook for gestioCOF project.
#
# Run formatters first, then checkers.
# Formatters which changed a file must set the flag 'formatter_updated'.
exit_code=0
formatter_updated=0
checker_dirty=0
# TODO(AD): We should check only staged changes.
# Working? -> Stash unstaged changes, run it, pop stash
STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$")
# Formatter: black
printf "> black ... "
if type black &>/dev/null; then
if [ -z "$STAGED_PYTHON_FILES" ]; then
printf "OK\n"
else
BLACK_OUTPUT="/tmp/gc-black-output.log"
touch $BLACK_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black --check &>$BLACK_OUTPUT; then
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black &>$BLACK_OUTPUT
tail -1 $BLACK_OUTPUT
formatter_updated=1
else
printf "OK\n"
fi
fi
else
printf "SKIP: program not found\n"
printf "HINT: Install black with 'pip3 install black' (black requires Python>=3.6)\n"
fi
# Formatter: isort
printf "> isort ... "
if type isort &>/dev/null; then
if [ -z "$STAGED_PYTHON_FILES" ]; then
printf "OK\n"
else
ISORT_OUTPUT="/tmp/gc-isort-output.log"
touch $ISORT_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
printf "Reformatted.\n"
formatter_updated=1
else
printf "OK\n"
fi
fi
else
printf "SKIP: program not found\n"
printf "HINT: Install isort with 'pip install isort'\n"
fi
# Checker: flake8
printf "> flake8 ... "
if type flake8 &>/dev/null; then
if [ -z "$STAGED_PYTHON_FILES" ]; then
printf "OK\n"
else
FLAKE8_OUTPUT="/tmp/gc-flake8-output.log"
touch $FLAKE8_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then
printf "FAIL\n"
cat $FLAKE8_OUTPUT
checker_dirty=1
else
printf "OK\n"
fi
fi
else
printf "SKIP: program not found\n"
printf "HINT: Install flake8 with 'pip install flake8'\n"
fi
# End
if [ $checker_dirty -ne 0 ]
then
printf ">>> Checker(s) detect(s) issue(s)\n"
printf " You can still commit and push :)\n"
printf " Be warned that our CI may cause you more trouble.\n"
fi
if [ $formatter_updated -ne 0 ]
then
printf ">>> Working tree updated by formatter(s)\n"
printf " Add changes to staging area and retry.\n"
exit_code=1
fi
printf "\n"
exit $exit_code

View file

@ -1,298 +0,0 @@
# Changelog
Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
2018).
## Le FUTUR ! (pas prêt pour la prod)
### Nouveau module de gestion des événements
- Désormais complet niveau modèles
- Export des participants implémenté
#### TODO
- Vue de création d'événements ergonomique
- Vue d'inscription à un événement **ou** intégration propre dans la vue
"inscription d'un nouveau membre"
### Nouveau module de gestion des clubs
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
adhérents ni des cotisations.
## TODO Prod
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
## Version ??? - ??/??/????
## Version 0.15.1 - 15/06/2023
### K-Fêt
- Rattrape les erreurs d'envoi de mail de négatif
- Utilise l'adresse chefs pour les envois de négatifs
## Version 0.15 - 22/05/2023
### K-Fêt
- Rajoute un formulaire de contact
- Rajoute un formulaire de demande de soirée
- Désactive les mails d'envoi de négatifs sur les comptes gelés
## Version 0.14 - 19/05/2023
- Répare les dépendances en spécifiant toutes les versions
### K-Fêt
- Répare la gestion des changement d'heure via moment.js
## Version 0.13 - 19/02/2023
### K-Fêt
- Rajoute la valeur des inventaires
- Résout les problèmes de négatif ne disparaissant pas
- Affiche son surnom s'il y en a un
- Bugfixes
## Version 0.12.1 - 03/10/2022
### K-Fêt
- Fixe un problème de rendu causé par l'agrandissement du menu
## Version 0.12 - 17/06/2022
### K-Fêt
- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13`
- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt
## Version 0.11 - 26/10/2021
### COF
- Répare un problème de rendu sur le wagtail du COF
### K-Fêt
- Ajoute de mails de rappels pour les comptes en négatif
- La recherche de comptes sur K-Psul remarche normalement
- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion
- Modification du gel de compte:
- on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt")
- on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul
- les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement
- Modification du fonctionnement des négatifs
- impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount`
- il n'y a plus de limite de temps sur les négatifs
- supression des autorisations de négatif
- il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge
- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites
- La supression d'opérations anciennes est réparée
## Version 0.10 - 18/04/2021
### K-Fêt
- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à
la fois.
- L'interface indique plus clairement quand on fait une erreur en modifiant un
compte.
- On supprime la fonction "décalage de balance".
- L'accès à l'historique est maintenant limité à 7 jours pour raison de
confidentialité. Les chefs/trez peuvent disposer d'une permission
supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta.
L'accès à son historique personnel n'est pas limité. Les durées sont
configurables dans `settings/cof_prod.py`.
### COF
- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha
au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices
## Version 0.9 - 06/02/2020
### COF / BdA
- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par
KDE).
- La page d'accueil affiche la date de fermeture des tirages BdA.
- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes
ses places pour pouvoir revendre.
- On s'assure que l'email fourni lors d'une demande de petit cours est valide.
### BDS
- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô
en modifiant le profil d'un membre du BDS.
- Le burô peut exporter la liste de ses membres avec email au format CSV depuis
la page d'accueil.
### K-Fêt
- On affiche les articles actuellement en vente en premier lors des inventaires
et des commandes.
- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier
inventaire sont affectés.
## Version 0.8 - 03/12/2020
### COF
- La page "Mes places" dans la section BdA indique quelles places sont sur
listing.
- ergonomie de l'interface admin du BdA : moins d'options inutiles lors de
la sélection de participants.
- les tirages sont maintenant archivables pour éviter d'avoir encore d'autres
options inutiles.
- l'autocomplétion dans l'admin BdA est réparée.
- Les icones de la page de gestion des petits cours sont (à nouveau) réparées.
- On a supprimé la possibilité de modifier les mails automatiques depuis
l'interface admin car trop problématique. Faute de mieux, envoyer un mail à
KDE pour modifier ces mails.
- corrige un crash sporadique sur la page d'inscription au système de petits
cours
### K-Fêt
- (fix partiel) Empêche la K-Fêt de modifier des données COF (e.g. nom, prénom,
username) lors de la création d'un nouveau compte.
- Les statistiques de conso globales montrent deux courbes COF / non-COF au
lieu de LIQ / sur compte.
- Un bug empêchait de fermer manuellement la K-Fêt depuis un compte non
privilégié en tapant un mot de passe. C'est corrigé.
## Version 0.7.2 - 08/09/2020
- Nouvelle page 404
- Correction de bug en K-Fêt : le lien pour créer un nouveau compte exté apparaît
à nouveau dans l'autocomplétion
## Version 0.7.1 - 05/09/2020
Petits ajustements sur le site du COF :
- Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et
de la page web dans les annuaires (clubs et partenaires).
- Corrige un bug d'affichage des adresses emails de clubs
## Version 0.7 - 29/08/2020
### GestioBDS
- Ajout d'un bouton pour supprimer un compte
- Le nombre d'adhérent⋅es est affiché sur la page d'accueil
- le groupe BDS a les bonnes permissions
### Site du COF
- Captcha fonctionnel pour les mailing-listes
### K-Fêt
- L'autocomplétion pour la création de compte K-Fêt se lance à 3 caractères seulement,
donc est plus rapide.
## Version 0.6 - 27/07/2020
Arrivée du BDS !
GestioCOF et GestioBDS ont du code en commun mais tournent de façon séparée, les
deux bases de données sont distinctes.
## Version 0.5 - 11/07/2020
### Problèmes corrigés
- La recherche d'utilisateurices (COF + K-Fêt) fonctionne de nouveau
- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur
la page des clubs (nouveau site du COF)
- Version mobile plus ergonimique sur le nouveau site du COF
- Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus
d'erreurs 500 (nouveau site du COF)
- L'historique des ventes des articles K-Fêt fonctionne à nouveau
- Les montants en K-Fêt sont à nouveau affichés en UKF (et non en €).
- Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un
spectacle BdA fonctionnent à nouveau.
- on ne peut plus compter de consos sur ☠☠☠, ni éditer les comptes spéciaux
(LIQ, GNR, ☠☠☠, #13).
### Nouvelles fonctionnalités
- On n'affiche que 4 articles sur la pages "nouveautés" (nouveau site du COF)
- Plus de traductions sur le nouveau site du COF
- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique
personnel.
- les statistiques K-Fêt remontent à plus d'un an (et le code est simplifié)
## Version 0.4.1 - 17/01/2020
- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés
aux urls (\#, /...)
## Version 0.4 - 15/01/2020
- Corrige un bug d'affichage d'images sur l'interface des petits cours
- La page des transferts permet de créer un nombre illimité de transferts en
une fois.
- Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs
- Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil
- Certaines opérations sont à nouveau accessibles depuis la session partagée
K-Fêt.
- Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper
- Corrige un crash sur la page des reventes pour les nouveaux participants.
- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux
## Version 0.3.3 - 30/11/2019
- Corrige un problème de redirection lors de la déconnexion (CAS seulement)
- Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF
- Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ
- Corrige une privilege escalation liée aux sessions partagées en K-Fêt
https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240
## Version 0.3.2 - 04/11/2019
- Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom
## Version 0.3.1 - 19/10/2019
- Bugfix: l'historique des utilisateurices s'affiche à nouveau
## Version 0.3 - 16/10/2019
- Comptes extés: lien pour changer son mot de passe sur la page d'accueil
- Les utilisateurices non-COF peuvent éditer leur profil
- Un peu de pub pour KDEns sur la page d'accueil
- Fix erreur 500 sur /bda/revente/<tirage_id>/manage
- Si on essaie d'accéder au compte que qqn d'autre on a une 404 (et plus une 403)
- On ne peut plus modifier des comptes COF depuis l'interface K-Fêt
- Le champ de paiement BdA se fait au niveau des attributions
- Affiche un message d'erreur plutôt que de crasher si échec de l'envoi du mail
de bienvenue aux nouveaux membres
- On peut supprimer des comptes et des articles K-Fêt
- Passage à Django2
- Dev : on peut désactiver la barre de debug avec une variable shell
- Remplace les CSS de Google par des polices de proximité
- Passage du site du COF et de la K-Fêt en Wagtail 2.3 et Wagtail-modeltranslation 0.9
- Ajoute un lien vers l'administration générale depuis les petits cours
- Abandon de l'ancien catalogue BdA (déjà plus utilisé depuis longtemps)
- Force l'unicité des logins clipper
- Nouveau site du COF en wagtail
- Meilleurs affichage des longues listes de spectacles à cocher dans BdA-Revente
- Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du
COF
## Version 0.2 - 07/11/2018
- Corrections de bugs d'interface dans l'inscription aux tirages BdA
- On peut annuler une revente à tout moment
- Pleiiiiin de tests
## Version 0.1 - 09/09/2018
Début de la numérotation des versions, début du changelog

162
README.md
View file

@ -1,86 +1,17 @@
# GestioCOF / GestioBDS
[![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
# GestioCOF
## Installation
Il est possible d'installer GestioCOF sur votre machine de deux façons différentes :
- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère
- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde
### Installation manuelle
Il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python ainsi
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python3-pip python3-dev python3-venv sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
python3 -m venv venv
Pour l'activer, il faut taper
. venv/bin/activate
depuis le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -U pip # parfois nécessaire la première fois
pip install -r requirements-devel.txt
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py gestioasso/settings/secret.py
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
encore l'installation *via* un lien symbolique:
ln -s ../../.pre-commit.sh .git/hooks/pre-commit
Pour plus d'informations à ce sujet, consulter la
[page](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/coding-style)
du wiki gestioCOF liée aux conventions.
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
données bidons bien pratiques pour développer avec la commande suivante :
bash provisioning/prepare_django.sh
Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas
pour plus de détails.
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
### Vagrant
Une autre façon d'installer GestioCOF sur votre machine est d'utiliser
La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser
[Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
le monde à la même configuration de développement (même sous Windows !), et
l'installation se fait en une commande.
Pour utiliser Vagrant, il faut le
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
Si vous êtes sous Linux, votre distribution propose probablement des paquets
Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui
@ -150,6 +81,55 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
code change, il faut relancer le worker avec `sudo systemctl restart
worker.service` pour visualiser la dernière version du code.
### Installation manuelle
Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant,
il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python ainsi
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python3-pip python3-dev sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant :
python3 -m venv venv
Pour l'activer, il faut faire
. venv/bin/activate
dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` :
pip install -U pip
pip install -r requirements-devel.txt
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py cof/settings/secret.py
#### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
données bidons bien pratiques pour développer avec la commande suivante :
bash provisioning/prepare_django.sh
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
python manage.py runserver
### Mise à jour
Pour mettre à jour les paquets Python, utiliser la commande suivante :
@ -161,44 +141,6 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire :
python manage.py migrate
## Outils pour développer
### Base de donnée
Quelle que soit la méthode d'installation choisie, la base de donnée locale est
peuplée avec des données artificielles pour faciliter le développement.
- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez
vous sur ce compte pour accéder à tout GestioCOF.
- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques
spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA.
- À chaque compte est associé un trigramme K-Fêt
- Un certain nombre d'articles K-Fêt sont renseignés.
### Tests unitaires
On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab
à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante.
Pour lancer les tests :
```
python manage.py test
```
### Astuces
- En développement on utilise la django debug toolbar parce que c'est utile pour
débuguer les templates ou les requêtes SQL mais des fois c'est pénible parce
ça fait ramer GestioCOF (surtout dans wagtail).
Vous pouvez la désactiver temporairement en définissant la variable
d'environnement `DJANGO_NO_DDT` dans votre shell : par exemple dans
bash/zsh/…:
```
$ export DJANGO_NO_DDT=1
```
## Documentation utilisateur
Une brève documentation utilisateur est accessible sur le

42
Vagrantfile vendored
View file

@ -1,19 +1,47 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Configuration de base pour GestioCOF.
# Voir https://docs.vagrantup.com pour plus d'informations.
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure(2) do |config|
# On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en
# production.
config.vm.box = "debian/contrib-buster64"
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.box = "ubuntu/xenial64"
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
# ordinateur, et le port 8000 avec le port 8000.
config.vm.network :forwarded_port, guest: 80, host: 8080
config.vm.network :forwarded_port, guest: 8000, host: 8000
# Le restes de la configuration (installation de paquets, etc) est géré un
# script shell.
# Create a private network, which allows host-only access to the machine
# using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
#
# View the documentation for the provider you are using for more
# information on available options.
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# sudo apt-get update
# sudo apt-get install -y apache2
# SHELL
config.vm.provision :shell, path: "provisioning/bootstrap.sh"
end

View file

@ -0,0 +1 @@

View file

@ -1,25 +1,18 @@
from datetime import timedelta
# -*- coding: utf-8 -*-
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin
from django.core.mail import send_mass_mail
from django.db.models import Count, Q, Sum
from django.template import loader
from django.db.models import Sum, Count
from django.template.defaultfilters import pluralize
from django.utils import timezone
from django import forms
from bda.models import (
Attribution,
CategorieSpectacle,
ChoixSpectacle,
Participant,
Quote,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
from dal.autocomplete import ModelSelect2
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
class ReadOnlyMixin(object):
@ -33,15 +26,29 @@ class ReadOnlyMixin(object):
return readonly_fields + self.readonly_fields_update
class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta:
widgets = {
'participant': ModelSelect2(url='bda-participant-autocomplete'),
'spectacle': ModelSelect2(url='bda-spectacle-autocomplete'),
}
class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle
form = ChoixSpectacleAdminForm
sortable_field_name = "priority"
class AttributionTabularAdminForm(forms.ModelForm):
listing = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
spectacles = Spectacle.objects.select_related("location")
spectacles = Spectacle.objects.select_related('location')
if self.listing is not None:
spectacles = spectacles.filter(listing=self.listing)
self.fields["spectacle"].queryset = spectacles
self.fields['spectacle'].queryset = spectacles
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
@ -65,117 +72,72 @@ class AttributionInline(admin.TabularInline):
class WithListingAttributionInline(AttributionInline):
exclude = ("given",)
exclude = ('given', )
form = WithListingAttributionTabularAdminForm
listing = True
verbose_name_plural = "Attributions sur listing"
class WithoutListingAttributionInline(AttributionInline):
form = WithoutListingAttributionTabularAdminForm
listing = False
verbose_name_plural = "Attributions hors listing"
class ParticipantAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
queryset = Spectacle.objects.select_related("location")
if self.instance.pk is not None:
queryset = queryset.filter(tirage=self.instance.tirage)
self.fields["choicesrevente"].queryset = queryset
class ParticipantPaidFilter(admin.SimpleListFilter):
"""
Permet de filtrer les participants sur s'ils ont payé leurs places ou pas
"""
title = "A payé"
parameter_name = "paid"
def lookups(self, request, model_admin):
return ((True, "Oui"), (False, "Non"))
def queryset(self, request, queryset):
return queryset.filter(paid=self.value())
self.fields['choicesrevente'].queryset = (
Spectacle.objects
.select_related('location')
)
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
def get_queryset(self, request):
return self.model.objects.annotate_paid().annotate(
nb_places=Count("attributions"),
remain=Sum(
"attribution__spectacle__price", filter=Q(attribution__paid=False)
),
total=Sum("attributions__price"),
)
return Participant.objects.annotate(nb_places=Count('attributions'),
total=Sum('attributions__price'))
def nb_places(self, obj):
return obj.nb_places
nb_places.admin_order_field = "nb_places"
nb_places.short_description = "Nombre de places"
def paid(self, obj):
return obj.paid
paid.short_description = "A payé"
paid.boolean = True
paid.admin_order_field = "paid"
def total(self, obj):
tot = obj.total
if tot:
return "%.02f" % tot
else:
return "0 €"
total.admin_order_field = "total"
total.short_description = "Total des places"
def remain(self, obj):
rem = obj.remain
if rem:
return "%.02f" % rem
else:
return "0 €"
remain.admin_order_field = "remain"
remain.short_description = "Reste à payer"
list_display = ("user", "nb_places", "total", "paid", "remain", "tirage")
list_filter = (ParticipantPaidFilter, "tirage")
search_fields = ("user__username", "user__first_name", "user__last_name")
actions = ["send_attribs"]
total.short_description = "Total à payer"
list_display = ("user", "nb_places", "total", "paid", "paymenttype",
"tirage")
list_filter = ("paid", "tirage")
search_fields = ('user__username', 'user__first_name', 'user__last_name')
actions = ['send_attribs', ]
actions_on_bottom = True
list_per_page = 400
readonly_fields = ("total", "paid")
readonly_fields_update = ("user", "tirage")
readonly_fields = ("total",)
readonly_fields_update = ('user', 'tirage')
form = ParticipantAdminForm
def send_attribs(self, request, queryset):
emails = []
datatuple = []
for member in queryset.all():
subject = "Résultats du tirage au sort"
attribs = member.attributions.all()
context = {"member": member.user}
template_name = ""
context = {'member': member.user}
shortname = ""
if len(attribs) == 0:
template_name = "bda/mails/attributions-decus.txt"
shortname = "bda-attributions-decus"
else:
template_name = "bda/mails/attributions.txt"
context["places"] = attribs
message = loader.render_to_string(template_name, context)
emails.append((subject, message, "bda@ens.fr", [member.user.email]))
send_mass_mail(emails)
shortname = "bda-attributions"
context['places'] = attribs
print(context)
datatuple.append((shortname, context, "bda@ens.fr",
[member.user.email]))
send_mass_custom_mail(datatuple)
count = len(queryset.all())
if count == 1:
message_bit = "1 membre a"
@ -183,59 +145,63 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
else:
message_bit = "%d membres ont" % count
plural = "s"
self.message_user(
request, "%s été informé%s avec succès." % (message_bit, plural)
)
self.message_user(request, "%s été informé%s avec succès."
% (message_bit, plural))
send_attribs.short_description = "Envoyer les résultats par mail"
class AttributionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'spectacle' in self.fields:
self.fields['spectacle'].queryset = (
Spectacle.objects
.select_related('location')
)
if 'participant' in self.fields:
self.fields['participant'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
def clean(self):
cleaned_data = super().clean()
cleaned_data = super(AttributionAdminForm, self).clean()
participant = cleaned_data.get("participant")
spectacle = cleaned_data.get("spectacle")
if participant and spectacle:
if participant.tirage != spectacle.tirage:
raise forms.ValidationError(
"Erreur : le participant et le spectacle n'appartiennent"
"pas au même tirage"
)
"pas au même tirage")
return cleaned_data
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
def paid(self, obj):
return obj.participant.paid
paid.short_description = 'A payé'
paid.boolean = True
list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = (
"spectacle__title",
"participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
)
search_fields = ('spectacle__title', 'participant__user__username',
'participant__user__first_name',
'participant__user__last_name')
form = AttributionAdminForm
readonly_fields_update = ("spectacle", "participant")
readonly_fields_update = ('spectacle', 'participant')
class ChoixSpectacleAdmin(admin.ModelAdmin):
autocomplete_fields = ["participant", "spectacle"]
form = ChoixSpectacleAdminForm
def tirage(self, obj):
return obj.participant.tirage
list_display = ("participant", "tirage", "spectacle", "priority", "double_choice")
list_display = ("participant", "tirage", "spectacle", "priority",
"double_choice")
list_filter = ("double_choice", "participant__tirage")
search_fields = (
"participant__user__username",
"participant__user__first_name",
"participant__user__last_name",
"spectacle__title",
)
search_fields = ('participant__user__username',
'participant__user__first_name',
'participant__user__last_name',
'spectacle__title')
class QuoteInline(admin.TabularInline):
@ -245,43 +211,49 @@ class QuoteInline(admin.TabularInline):
class SpectacleAdmin(admin.ModelAdmin):
inlines = [QuoteInline]
model = Spectacle
list_display = ("title", "date", "tirage", "location", "slots", "price", "listing")
list_filter = ("location", "tirage")
list_display = ("title", "date", "tirage", "location", "slots", "price",
"listing")
list_filter = ("location", "tirage",)
search_fields = ("title", "location__name")
readonly_fields = ("rappel_sent",)
readonly_fields = ("rappel_sent", )
class TirageAdmin(admin.ModelAdmin):
model = Tirage
list_display = ("title", "ouverture", "fermeture", "active", "enable_do_tirage")
readonly_fields = ("tokens",)
list_filter = ("active",)
search_fields = ("title",)
list_display = ("title", "ouverture", "fermeture", "active",
"enable_do_tirage")
readonly_fields = ("tokens", )
list_filter = ("active", )
search_fields = ("title", )
class SalleAdmin(admin.ModelAdmin):
model = Salle
search_fields = ("name", "address")
search_fields = ('name', 'address')
class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
qset = Participant.objects.select_related("user", "tirage")
if self.instance.pk is not None:
qset = qset.filter(tirage=self.instance.seller.tirage)
self.fields["confirmed_entry"].queryset = qset
self.fields["seller"].queryset = qset
self.fields["soldTo"].queryset = qset
self.fields['answered_mail'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
self.fields['seller'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
self.fields['soldTo'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
class SpectacleReventeAdmin(admin.ModelAdmin):
"""
Administration des reventes de spectacles
"""
model = SpectacleRevente
def spectacle(self, obj):
@ -293,14 +265,12 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
list_display = ("spectacle", "seller", "date", "soldTo")
raw_id_fields = ("attribution",)
readonly_fields = ("date_tirage",)
search_fields = [
"attribution__spectacle__title",
"seller__user__username",
"seller__user__first_name",
"seller__user__last_name",
]
search_fields = ['attribution__spectacle__title',
'seller__user__username',
'seller__user__first_name',
'seller__user__last_name']
actions = ["transfer", "reinit"]
actions = ['transfer', 'reinit']
actions_on_bottom = True
form = SpectacleReventeAdminForm
@ -316,10 +286,10 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
attrib.save()
self.message_user(
request,
"%d attribution%s %s été transférée%s avec succès."
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
)
"%d attribution%s %s été transférée%s avec succès." % (
count, pluralize(count),
pluralize(count, "a,ont"), pluralize(count))
)
transfer.short_description = "Transférer les reventes sélectionnées"
def reinit(self, request, queryset):
@ -328,15 +298,20 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
"""
count = queryset.count()
for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()
):
revente.reset(new_date=timezone.now() - timedelta(hours=1))
attribution__spectacle__date__gte=timezone.now()):
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
self.message_user(
request,
"%d attribution%s %s été réinitialisée%s avec succès."
% (count, pluralize(count), pluralize(count, "a,ont"), pluralize(count)),
)
"%d attribution%s %s été réinitialisée%s avec succès." % (
count, pluralize(count),
pluralize(count, "a,ont"), pluralize(count))
)
reinit.short_description = "Réinitialiser les reventes sélectionnées"

View file

@ -1,7 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db.models import Max
import random
class Algorithm(object):
shows = None
ranks = None
origranks = None
@ -9,11 +18,11 @@ class Algorithm(object):
def __init__(self, shows, members, choices):
"""Initialisation :
- on aggrège toutes les demandes pour chaque spectacle dans
show.requests
- on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings"""
self.max_group = 2 * max(choice.priority for choice in choices)
- on aggrège toutes les demandes pour chaque spectacle dans
show.requests
- on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings"""
self.max_group = 2*max(choice.priority for choice in choices)
self.shows = []
showdict = {}
for show in shows:
@ -51,19 +60,16 @@ class Algorithm(object):
self.ranks[member][show] -= increment
def appendResult(self, l, member, show):
l.append(
(
member,
self.ranks[member][show],
self.origranks[member][show],
self.choices[member][show].double,
)
)
l.append((member,
self.ranks[member][show],
self.origranks[member][show],
self.choices[member][show].double))
def __call__(self, seed):
random.seed(seed)
results = []
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots, reverse=True)
shows = sorted(self.shows, key=lambda x: x.nrequests / x.slots,
reverse=True)
for show in shows:
# On regroupe tous les gens ayant le même rang
groups = dict([(i, []) for i in range(1, self.max_group + 1)])
@ -82,10 +88,8 @@ class Algorithm(object):
if len(winners) + 1 < show.slots:
self.appendResult(winners, member, show)
self.appendResult(winners, member, show)
elif (
not self.choices[member][show].autoquit
and len(winners) < show.slots
):
elif not self.choices[member][show].autoquit \
and len(winners) < show.slots:
self.appendResult(winners, member, show)
self.appendResult(losers, member, show)
else:

View file

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
from django import forms
from django.forms.models import BaseInlineFormSet
from django.template import loader
from django.utils import timezone
from bda.models import SpectacleRevente
from bda.models import Attribution, Spectacle
class InscriptionInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -16,9 +18,9 @@ class InscriptionInlineFormSet(BaseInlineFormSet):
# set once for all "spectacle" field choices
# - restrict choices to the spectacles of this tirage
# - force_choices avoid many db requests
spectacles = tirage.spectacle_set.select_related("location")
spectacles = tirage.spectacle_set.select_related('location')
choices = [(sp.pk, str(sp)) for sp in spectacles]
self.force_choices("spectacle", choices)
self.force_choices('spectacle', choices)
def force_choices(self, name, choices):
"""Set choices of a field.
@ -30,7 +32,7 @@ class InscriptionInlineFormSet(BaseInlineFormSet):
for form in self.forms:
field = form.fields[name]
if field.empty_label is not None:
field.choices = [("", field.empty_label)] + choices
field.choices = [('', field.empty_label)] + choices
else:
field.choices = choices
@ -39,146 +41,77 @@ class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea())
class TemplateLabelField(forms.ModelMultipleChoiceField):
"""
Extends ModelMultipleChoiceField to offer two more customization options :
- `label_from_instance` can be used with a template file
- the widget rendering template can be specified with `option_template_name`
"""
def __init__(
self,
label_template_name=None,
context_object_name="obj",
option_template_name=None,
*args,
**kwargs
):
super().__init__(*args, **kwargs)
self.label_template_name = label_template_name
self.context_object_name = context_object_name
if option_template_name is not None:
self.widget.option_template_name = option_template_name
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
if self.label_template_name is None:
return super().label_from_instance(obj)
else:
return loader.render_to_string(
self.label_template_name, context={self.context_object_name: obj}
)
# Formulaires pour revente_manage
return "%s" % str(obj.spectacle)
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["attributions"] = TemplateLabelField(
queryset=participant.attribution_set.filter(
spectacle__date__gte=timezone.now(), paid=True
)
super(ResellForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now())
.exclude(revente__seller=participant)
.select_related("spectacle", "spectacle__location", "participant__user"),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/attribution_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="attribution",
.select_related('spectacle', 'spectacle__location',
'participant__user')
)
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
label="",
queryset=participant.original_shows.filter(
attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
)
.order_by("-date"),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_self_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
super(AnnulForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__notif_sent=False,
revente__soldTo__isnull=True)
.select_related('spectacle', 'spectacle__location',
'participant__user')
)
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, tirage, *args, **kwargs):
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
self.fields['spectacles'].queryset = (
tirage.spectacle_set
.select_related('location')
.filter(date__gte=timezone.now())
)
class SoldForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label='',
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=participant.original_shows.filter(soldTo__isnull=False)
.exclude(soldTo=participant)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
),
widget=forms.CheckboxSelectMultiple,
label_template_name="bda/forms/revente_sold_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
# Formulaire pour revente_subscribe
class InscriptionReventeForm(forms.Form):
def __init__(self, tirage, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["spectacles"] = TemplateLabelField(
queryset=tirage.spectacle_set.select_related("location").filter(
date__gte=timezone.now()
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/spectacle_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="spectacle",
)
# Formulaires pour revente_tirages
class ReventeTirageAnnulForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=participant.entered.filter(soldTo__isnull=True).select_related(
"attribution__spectacle", "seller__user"
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_other_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
class ReventeTirageForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=(
SpectacleRevente.objects.filter(
notif_sent=True,
shotgun=False,
tirage_done=False,
attribution__spectacle__tirage=participant.tirage,
)
.exclude(confirmed_entry=participant)
.select_related("attribution__spectacle")
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_other_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
super(SoldForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = (
participant.attribution_set
.filter(revente__isnull=False,
revente__soldTo__isnull=False)
.exclude(revente__soldTo=participant)
.select_related('spectacle', 'spectacle__location',
'participant__user')
)

View file

@ -5,15 +5,17 @@ Crée deux tirages de test et y inscrit les utilisateurs
import os
import random
from django.contrib.auth.models import User
from django.utils import timezone
from django.contrib.auth.models import User
from bda.models import ChoixSpectacle, Participant, Salle, Spectacle, Tirage
from bda.views import do_tirage
from gestioncof.management.base import MyBaseCommand
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
from bda.views import do_tirage
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'data')
class Command(MyBaseCommand):
@ -25,29 +27,27 @@ class Command(MyBaseCommand):
# ---
Tirage.objects.all().delete()
Tirage.objects.bulk_create(
[
Tirage(
title="Tirage de test 1",
ouverture=timezone.now() - timezone.timedelta(days=7),
fermeture=timezone.now(),
active=True,
),
Tirage(
title="Tirage de test 2",
ouverture=timezone.now(),
fermeture=timezone.now() + timezone.timedelta(days=60),
active=True,
),
]
)
Tirage.objects.bulk_create([
Tirage(
title="Tirage de test 1",
ouverture=timezone.now()-timezone.timedelta(days=7),
fermeture=timezone.now(),
active=True
),
Tirage(
title="Tirage de test 2",
ouverture=timezone.now(),
fermeture=timezone.now()+timezone.timedelta(days=60),
active=True
)
])
tirages = Tirage.objects.all()
# ---
# Salles
# ---
locations = self.from_json("locations.json", DATA_DIR, Salle)
locations = self.from_json('locations.json', DATA_DIR, Salle)
# ---
# Spectacles
@ -60,13 +60,15 @@ class Command(MyBaseCommand):
"""
show.tirage = random.choice(tirages)
show.listing = bool(random.randint(0, 1))
show.date = show.tirage.fermeture + timezone.timedelta(
days=random.randint(60, 90)
show.date = (
show.tirage.fermeture
+ timezone.timedelta(days=random.randint(60, 90))
)
show.location = random.choice(locations)
return show
shows = self.from_json("shows.json", DATA_DIR, Spectacle, show_callback)
shows = self.from_json(
'shows.json', DATA_DIR, Spectacle, show_callback
)
# ---
# Inscriptions
@ -77,19 +79,23 @@ class Command(MyBaseCommand):
choices = []
for user in User.objects.filter(profile__is_cof=True):
for tirage in tirages:
part, _ = Participant.objects.get_or_create(user=user, tirage=tirage)
shows = random.sample(
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
part, _ = Participant.objects.get_or_create(
user=user,
tirage=tirage
)
for rank, show in enumerate(shows):
choices.append(
ChoixSpectacle(
participant=part,
spectacle=show,
priority=rank + 1,
double_choice=random.choice(["1", "double", "autoquit"]),
shows = random.sample(
list(tirage.spectacle_set.all()),
tirage.spectacle_set.count() // 2
)
for (rank, show) in enumerate(shows):
choices.append(ChoixSpectacle(
participant=part,
spectacle=show,
priority=rank + 1,
double_choice=random.choice(
['1', 'double', 'autoquit']
)
)
))
ChoixSpectacle.objects.bulk_create(choices)
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))

View file

@ -1,49 +1,43 @@
# -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des reventes.
"""
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management import BaseCommand
from django.utils import timezone
from bda.models import SpectacleRevente
class Command(BaseCommand):
help = (
"Envoie les mails de notification et effectue les tirages au sort des reventes"
)
help = "Envoie les mails de notification et effectue " \
"les tirages au sort des reventes"
leave_locale_alone = True
def handle(self, *args, **options):
now = timezone.now()
reventes = SpectacleRevente.objects.all()
for revente in reventes:
# Le spectacle est bientôt et on a pas encore envoyé de mail :
# on met la place au shotgun et on prévient.
if revente.is_urgent and not revente.notif_sent:
if revente.can_notif:
self.stdout.write(str(now))
revente.mail_shotgun()
self.stdout.write(
"Mails de disponibilité immédiate envoyés "
"pour la revente [%s]" % revente
)
# Le spectacle est dans plus longtemps : on prévient
elif revente.can_notif and not revente.notif_sent:
# Check si < 24h
if (revente.attribution.spectacle.date <=
revente.date + timedelta(days=1)) and \
now >= revente.date + timedelta(minutes=15) and \
not revente.notif_sent:
self.stdout.write(str(now))
revente.mail_shotgun()
self.stdout.write("Mail de disponibilité immédiate envoyé")
# Check si délai de retrait dépassé
elif (now >= revente.date + timedelta(hours=1) and
not revente.notif_sent):
self.stdout.write(str(now))
revente.send_notif()
self.stdout.write(
"Mails d'inscription à la revente [%s] envoyés" % revente
)
# On fait le tirage
elif now >= revente.date_tirage and not revente.tirage_done:
self.stdout.write("Mail d'inscription à une revente envoyé")
# Check si tirage à faire
elif (now >= revente.date_tirage and
not revente.tirage_done):
self.stdout.write(str(now))
winner = revente.tirage()
self.stdout.write("Tirage effectué pour la revente [%s]" % revente)
if winner:
self.stdout.write("Gagnant : %s" % winner.user)
else:
self.stdout.write("Pas de gagnant ; place au shotgun")
revente.tirage()
self.stdout.write("Tirage effectué, mails envoyés")

View file

@ -1,33 +1,33 @@
# -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des mails de rappel.
"""
from datetime import timedelta
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from bda.models import Spectacle
class Command(BaseCommand):
help = (
"Envoie les mails de rappel des spectacles dont la date approche.\n"
"Ne renvoie pas les mails déjà envoyés."
)
help = 'Envoie les mails de rappel des spectacles dont la date ' \
'approche.\nNe renvoie pas les mails déjà envoyés.'
leave_locale_alone = True
def handle(self, *args, **options):
now = timezone.now()
delay = timedelta(days=4)
shows = (
Spectacle.objects.filter(date__range=(now, now + delay))
.filter(tirage__active=True)
.filter(rappel_sent__isnull=True)
shows = Spectacle.objects \
.filter(date__range=(now, now+delay)) \
.filter(tirage__active=True) \
.filter(rappel_sent__isnull=True) \
.all()
)
for show in shows:
show.send_rappel()
self.stdout.write("Mails de rappels pour %s envoyés avec succès." % show)
self.stdout.write(
'Mails de rappels pour %s envoyés avec succès.' % show)
if not shows:
self.stdout.write("Aucun mail à envoyer.")
self.stdout.write('Aucun mail à envoyer.')

View file

@ -1,205 +1,108 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Attribution",
name='Attribution',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("given", models.BooleanField(default=False, verbose_name="Donn\xe9e")),
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('given', models.BooleanField(default=False, verbose_name='Donn\xe9e')),
],
),
migrations.CreateModel(
name="ChoixSpectacle",
name='ChoixSpectacle',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"priority",
models.PositiveIntegerField(verbose_name=b"Priorit\xc3\xa9"),
),
(
"double_choice",
models.CharField(
default=b"1",
max_length=10,
verbose_name=b"Nombre de places",
choices=[
(b"1", b"1 place"),
(b"autoquit", b"2 places si possible, 1 sinon"),
(b"double", b"2 places sinon rien"),
],
),
),
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('priority', models.PositiveIntegerField(verbose_name=b'Priorit\xc3\xa9')),
('double_choice', models.CharField(default=b'1', max_length=10, verbose_name=b'Nombre de places', choices=[(b'1', b'1 place'), (b'autoquit', b'2 places si possible, 1 sinon'), (b'double', b'2 places sinon rien')])),
],
options={
"ordering": ("priority",),
"verbose_name": "voeu",
"verbose_name_plural": "voeux",
'ordering': ('priority',),
'verbose_name': 'voeu',
'verbose_name_plural': 'voeux',
},
),
migrations.CreateModel(
name="Participant",
name='Participant',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("paid", models.BooleanField(default=False, verbose_name="A pay\xe9")),
(
"paymenttype",
models.CharField(
blank=True,
max_length=6,
verbose_name="Moyen de paiement",
choices=[
(b"cash", "Cash"),
(b"cb", b"CB"),
(b"cheque", "Ch\xe8que"),
(b"autre", "Autre"),
],
),
),
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('paid', models.BooleanField(default=False, verbose_name='A pay\xe9')),
('paymenttype', models.CharField(blank=True, max_length=6, verbose_name='Moyen de paiement', choices=[(b'cash', 'Cash'), (b'cb', b'CB'), (b'cheque', 'Ch\xe8que'), (b'autre', 'Autre')])),
],
),
migrations.CreateModel(
name="Salle",
name='Salle',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=300, verbose_name=b"Nom")),
("address", models.TextField(verbose_name=b"Adresse")),
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=300, verbose_name=b'Nom')),
('address', models.TextField(verbose_name=b'Adresse')),
],
),
migrations.CreateModel(
name="Spectacle",
name='Spectacle',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("title", models.CharField(max_length=300, verbose_name=b"Titre")),
("date", models.DateTimeField(verbose_name=b"Date & heure")),
(
"description",
models.TextField(verbose_name=b"Description", blank=True),
),
(
"slots_description",
models.TextField(
verbose_name=b"Description des places", blank=True
),
),
(
"price",
models.FloatField(verbose_name=b"Prix d'une place", blank=True),
),
("slots", models.IntegerField(verbose_name=b"Places")),
(
"priority",
models.IntegerField(default=1000, verbose_name=b"Priorit\xc3\xa9"),
),
(
"location",
models.ForeignKey(to="bda.Salle", on_delete=models.CASCADE),
),
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=300, verbose_name=b'Titre')),
('date', models.DateTimeField(verbose_name=b'Date & heure')),
('description', models.TextField(verbose_name=b'Description', blank=True)),
('slots_description', models.TextField(verbose_name=b'Description des places', blank=True)),
('price', models.FloatField(verbose_name=b"Prix d'une place", blank=True)),
('slots', models.IntegerField(verbose_name=b'Places')),
('priority', models.IntegerField(default=1000, verbose_name=b'Priorit\xc3\xa9')),
('location', models.ForeignKey(to='bda.Salle', on_delete=models.CASCADE)),
],
options={
"ordering": ("priority", "date", "title"),
"verbose_name": "Spectacle",
'ordering': ('priority', 'date', 'title'),
'verbose_name': 'Spectacle',
},
),
migrations.AddField(
model_name="participant",
name="attributions",
field=models.ManyToManyField(
related_name="attributed_to",
through="bda.Attribution",
to="bda.Spectacle",
),
model_name='participant',
name='attributions',
field=models.ManyToManyField(related_name='attributed_to', through='bda.Attribution', to='bda.Spectacle'),
),
migrations.AddField(
model_name="participant",
name="choices",
field=models.ManyToManyField(
related_name="chosen_by",
through="bda.ChoixSpectacle",
to="bda.Spectacle",
),
model_name='participant',
name='choices',
field=models.ManyToManyField(related_name='chosen_by', through='bda.ChoixSpectacle', to='bda.Spectacle'),
),
migrations.AddField(
model_name="participant",
name="user",
field=models.OneToOneField(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
model_name='participant',
name='user',
field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
),
migrations.AddField(
model_name="choixspectacle",
name="participant",
field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
model_name='choixspectacle',
name='participant',
field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE),
),
migrations.AddField(
model_name="choixspectacle",
name="spectacle",
field=models.ForeignKey(
related_name="participants",
to="bda.Spectacle",
on_delete=models.CASCADE,
),
model_name='choixspectacle',
name='spectacle',
field=models.ForeignKey(related_name='participants', to='bda.Spectacle', on_delete=models.CASCADE),
),
migrations.AddField(
model_name="attribution",
name="participant",
field=models.ForeignKey(to="bda.Participant", on_delete=models.CASCADE),
model_name='attribution',
name='participant',
field=models.ForeignKey(to='bda.Participant', on_delete=models.CASCADE),
),
migrations.AddField(
model_name="attribution",
name="spectacle",
field=models.ForeignKey(
related_name="attribues", to="bda.Spectacle", on_delete=models.CASCADE
),
model_name='attribution',
name='spectacle',
field=models.ForeignKey(related_name='attribues', to='bda.Spectacle', on_delete=models.CASCADE),
),
migrations.AlterUniqueTogether(
name="choixspectacle", unique_together=set([("participant", "spectacle")])
name='choixspectacle',
unique_together=set([('participant', 'spectacle')]),
),
]

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
from django.conf import settings
from django.utils import timezone
@ -35,77 +35,50 @@ def fill_tirage_fields(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [("bda", "0001_initial")]
dependencies = [
('bda', '0001_initial'),
]
operations = [
migrations.CreateModel(
name="Tirage",
name='Tirage',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("title", models.CharField(max_length=300, verbose_name=b"Titre")),
(
"ouverture",
models.DateTimeField(
verbose_name=b"Date et heure d'ouverture du tirage"
),
),
(
"fermeture",
models.DateTimeField(
verbose_name=b"Date et heure de fermerture du tirage"
),
),
(
"token",
models.TextField(verbose_name=b"Graine du tirage", blank=True),
),
(
"active",
models.BooleanField(default=True, verbose_name=b"Tirage actif"),
),
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=300, verbose_name=b'Titre')),
('ouverture', models.DateTimeField(verbose_name=b"Date et heure d'ouverture du tirage")),
('fermeture', models.DateTimeField(verbose_name=b'Date et heure de fermerture du tirage')),
('token', models.TextField(verbose_name=b'Graine du tirage', blank=True)),
('active', models.BooleanField(default=True, verbose_name=b'Tirage actif')),
],
),
migrations.AlterField(
model_name="participant",
name="user",
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
),
model_name='participant',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
),
# Create fields `spectacle` for `Participant` and `Spectacle` models.
# These fields are not nullable, but we first create them as nullable
# to give a default value for existing instances of these models.
migrations.AddField(
model_name="participant",
name="tirage",
field=models.ForeignKey(
to="bda.Tirage", null=True, on_delete=models.CASCADE
),
model_name='participant',
name='tirage',
field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE),
),
migrations.AddField(
model_name="spectacle",
name="tirage",
field=models.ForeignKey(
to="bda.Tirage", null=True, on_delete=models.CASCADE
),
model_name='spectacle',
name='tirage',
field=models.ForeignKey(to='bda.Tirage', null=True, on_delete=models.CASCADE),
),
migrations.RunPython(fill_tirage_fields, migrations.RunPython.noop),
migrations.AlterField(
model_name="participant",
name="tirage",
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
model_name='participant',
name='tirage',
field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE),
),
migrations.AlterField(
model_name="spectacle",
name="tirage",
field=models.ForeignKey(to="bda.Tirage", on_delete=models.CASCADE),
model_name='spectacle',
name='tirage',
field=models.ForeignKey(to='bda.Tirage', on_delete=models.CASCADE),
),
]

View file

@ -5,17 +5,20 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0002_add_tirage")]
dependencies = [
('bda', '0002_add_tirage'),
]
operations = [
migrations.AlterField(
model_name="spectacle",
name="price",
model_name='spectacle',
name='price',
field=models.FloatField(verbose_name=b"Prix d'une place"),
),
migrations.AlterField(
model_name="tirage",
name="active",
field=models.BooleanField(default=False, verbose_name=b"Tirage actif"),
model_name='tirage',
name='active',
field=models.BooleanField(default=False, verbose_name=b'Tirage actif'),
),
]

View file

@ -5,22 +5,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0003_update_tirage_and_spectacle")]
dependencies = [
('bda', '0003_update_tirage_and_spectacle'),
]
operations = [
migrations.AddField(
model_name="spectacle",
name="listing",
field=models.BooleanField(
default=False, verbose_name=b"Les places sont sur listing"
),
model_name='spectacle',
name='listing',
field=models.BooleanField(default=False, verbose_name=b'Les places sont sur listing'),
preserve_default=False,
),
migrations.AddField(
model_name="spectacle",
name="rappel_sent",
field=models.DateTimeField(
null=True, verbose_name=b"Mail de rappel envoy\xc3\xa9", blank=True
),
model_name='spectacle',
name='rappel_sent',
field=models.DateTimeField(null=True, verbose_name=b'Mail de rappel envoy\xc3\xa9', blank=True),
),
]

View file

@ -5,24 +5,25 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0004_mails-rappel")]
dependencies = [
('bda', '0004_mails-rappel'),
]
operations = [
migrations.AlterField(
model_name="choixspectacle",
name="priority",
field=models.PositiveIntegerField(verbose_name="Priorit\xe9"),
model_name='choixspectacle',
name='priority',
field=models.PositiveIntegerField(verbose_name='Priorit\xe9'),
),
migrations.AlterField(
model_name="spectacle",
name="priority",
field=models.IntegerField(default=1000, verbose_name="Priorit\xe9"),
model_name='spectacle',
name='priority',
field=models.IntegerField(default=1000, verbose_name='Priorit\xe9'),
),
migrations.AlterField(
model_name="spectacle",
name="rappel_sent",
field=models.DateTimeField(
null=True, verbose_name="Mail de rappel envoy\xe9", blank=True
),
model_name='spectacle',
name='rappel_sent',
field=models.DateTimeField(null=True, verbose_name='Mail de rappel envoy\xe9', blank=True),
),
]

View file

@ -10,24 +10,26 @@ def forwards_func(apps, schema_editor):
db_alias = schema_editor.connection.alias
for tirage in Tirage.objects.using(db_alias).all():
if tirage.tokens:
tirage.tokens = 'Before %s\n"""%s"""\n' % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
tirage.tokens,
)
tirage.tokens = "Before %s\n\"\"\"%s\"\"\"\n" % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
tirage.tokens)
tirage.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0005_encoding")]
dependencies = [
('bda', '0005_encoding'),
]
operations = [
migrations.RenameField("tirage", "token", "tokens"),
migrations.RenameField('tirage', 'token', 'tokens'),
migrations.AddField(
model_name="tirage",
name="enable_do_tirage",
model_name='tirage',
name='enable_do_tirage',
field=models.BooleanField(
default=False, verbose_name=b"Le tirage peut \xc3\xaatre lanc\xc3\xa9"
),
default=False,
verbose_name=b'Le tirage peut \xc3\xaatre lanc\xc3\xa9'),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]

View file

@ -1,99 +1,91 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [("bda", "0006_add_tirage_switch")]
dependencies = [
('bda', '0006_add_tirage_switch'),
]
operations = [
migrations.CreateModel(
name="CategorieSpectacle",
name='CategorieSpectacle',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"name",
models.CharField(max_length=100, verbose_name="Nom", unique=True),
),
('id', models.AutoField(verbose_name='ID', serialize=False,
auto_created=True, primary_key=True)),
('name', models.CharField(max_length=100, verbose_name='Nom',
unique=True)),
],
options={"verbose_name": "Cat\xe9gorie"},
options={
'verbose_name': 'Cat\xe9gorie',
},
),
migrations.CreateModel(
name="Quote",
name='Quote',
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("text", models.TextField(verbose_name="Citation")),
("author", models.CharField(max_length=200, verbose_name="Auteur")),
('id', models.AutoField(verbose_name='ID', serialize=False,
auto_created=True, primary_key=True)),
('text', models.TextField(verbose_name='Citation')),
('author', models.CharField(max_length=200,
verbose_name='Auteur')),
],
),
migrations.AlterModelOptions(
name="spectacle",
options={"ordering": ("date", "title"), "verbose_name": "Spectacle"},
name='spectacle',
options={'ordering': ('date', 'title'),
'verbose_name': 'Spectacle'},
),
migrations.RemoveField(
model_name='spectacle',
name='priority',
),
migrations.RemoveField(model_name="spectacle", name="priority"),
migrations.AddField(
model_name="spectacle",
name="ext_link",
model_name='spectacle',
name='ext_link',
field=models.CharField(
max_length=500,
verbose_name="Lien vers le site du spectacle",
blank=True,
),
verbose_name='Lien vers le site du spectacle',
blank=True),
),
migrations.AddField(
model_name="spectacle",
name="image",
field=models.ImageField(
upload_to="imgs/shows/", null=True, verbose_name="Image", blank=True
),
model_name='spectacle',
name='image',
field=models.ImageField(upload_to='imgs/shows/', null=True,
verbose_name='Image', blank=True),
),
migrations.AlterField(
model_name="tirage",
name="enable_do_tirage",
model_name='tirage',
name='enable_do_tirage',
field=models.BooleanField(
default=False, verbose_name="Le tirage peut \xeatre lanc\xe9"
),
default=False,
verbose_name='Le tirage peut \xeatre lanc\xe9'),
),
migrations.AlterField(
model_name="tirage",
name="tokens",
field=models.TextField(verbose_name="Graine(s) du tirage", blank=True),
model_name='tirage',
name='tokens',
field=models.TextField(verbose_name='Graine(s) du tirage',
blank=True),
),
migrations.AddField(
model_name="spectacle",
name="category",
field=models.ForeignKey(
blank=True,
to="bda.CategorieSpectacle",
on_delete=models.CASCADE,
null=True,
),
model_name='spectacle',
name='category',
field=models.ForeignKey(blank=True, to='bda.CategorieSpectacle',
on_delete=models.CASCADE,
null=True),
),
migrations.AddField(
model_name="spectacle",
name="vips",
field=models.TextField(verbose_name="Personnalit\xe9s", blank=True),
model_name='spectacle',
name='vips',
field=models.TextField(verbose_name='Personnalit\xe9s',
blank=True),
),
migrations.AddField(
model_name="quote",
name="spectacle",
field=models.ForeignKey(to="bda.Spectacle", on_delete=models.CASCADE),
model_name='quote',
name='spectacle',
field=models.ForeignKey(to='bda.Spectacle',
on_delete=models.CASCADE),
),
]

View file

@ -1,109 +1,103 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [("bda", "0007_extends_spectacle")]
dependencies = [
('bda', '0007_extends_spectacle'),
]
operations = [
migrations.AlterField(
model_name="choixspectacle",
name="double_choice",
model_name='choixspectacle',
name='double_choice',
field=models.CharField(
verbose_name="Nombre de places",
choices=[
("1", "1 place"),
("autoquit", "2 places si possible, 1 sinon"),
("double", "2 places sinon rien"),
],
max_length=10,
default="1",
),
verbose_name='Nombre de places',
choices=[('1', '1 place'),
('autoquit', '2 places si possible, 1 sinon'),
('double', '2 places sinon rien')],
max_length=10, default='1'),
),
migrations.AlterField(
model_name="participant",
name="paymenttype",
model_name='participant',
name='paymenttype',
field=models.CharField(
blank=True,
choices=[
("cash", "Cash"),
("cb", "CB"),
("cheque", "Chèque"),
("autre", "Autre"),
],
max_length=6,
verbose_name="Moyen de paiement",
),
choices=[('cash', 'Cash'), ('cb', 'CB'),
('cheque', 'Chèque'), ('autre', 'Autre')],
max_length=6, verbose_name='Moyen de paiement'),
),
migrations.AlterField(
model_name="salle",
name="address",
field=models.TextField(verbose_name="Adresse"),
model_name='salle',
name='address',
field=models.TextField(verbose_name='Adresse'),
),
migrations.AlterField(
model_name="salle",
name="name",
field=models.CharField(verbose_name="Nom", max_length=300),
model_name='salle',
name='name',
field=models.CharField(verbose_name='Nom', max_length=300),
),
migrations.AlterField(
model_name="spectacle",
name="date",
field=models.DateTimeField(verbose_name="Date & heure"),
model_name='spectacle',
name='date',
field=models.DateTimeField(verbose_name='Date & heure'),
),
migrations.AlterField(
model_name="spectacle",
name="description",
field=models.TextField(verbose_name="Description", blank=True),
model_name='spectacle',
name='description',
field=models.TextField(verbose_name='Description', blank=True),
),
migrations.AlterField(
model_name="spectacle",
name="listing",
field=models.BooleanField(verbose_name="Les places sont sur listing"),
model_name='spectacle',
name='listing',
field=models.BooleanField(
verbose_name='Les places sont sur listing'),
),
migrations.AlterField(
model_name="spectacle",
name="price",
model_name='spectacle',
name='price',
field=models.FloatField(verbose_name="Prix d'une place"),
),
migrations.AlterField(
model_name="spectacle",
name="slots",
field=models.IntegerField(verbose_name="Places"),
model_name='spectacle',
name='slots',
field=models.IntegerField(verbose_name='Places'),
),
migrations.AlterField(
model_name="spectacle",
name="slots_description",
field=models.TextField(verbose_name="Description des places", blank=True),
model_name='spectacle',
name='slots_description',
field=models.TextField(verbose_name='Description des places',
blank=True),
),
migrations.AlterField(
model_name="spectacle",
name="title",
field=models.CharField(verbose_name="Titre", max_length=300),
model_name='spectacle',
name='title',
field=models.CharField(verbose_name='Titre', max_length=300),
),
migrations.AlterField(
model_name="tirage",
name="active",
field=models.BooleanField(verbose_name="Tirage actif", default=False),
model_name='tirage',
name='active',
field=models.BooleanField(verbose_name='Tirage actif',
default=False),
),
migrations.AlterField(
model_name="tirage",
name="fermeture",
model_name='tirage',
name='fermeture',
field=models.DateTimeField(
verbose_name="Date et heure de fermerture du tirage"
),
verbose_name='Date et heure de fermerture du tirage'),
),
migrations.AlterField(
model_name="tirage",
name="ouverture",
model_name='tirage',
name='ouverture',
field=models.DateTimeField(
verbose_name="Date et heure d'ouverture du tirage"
),
verbose_name="Date et heure d'ouverture du tirage"),
),
migrations.AlterField(
model_name="tirage",
name="title",
field=models.CharField(verbose_name="Titre", max_length=300),
model_name='tirage',
name='title',
field=models.CharField(verbose_name='Titre', max_length=300),
),
]

View file

@ -1,86 +1,69 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0008_py3")]
dependencies = [
('bda', '0008_py3'),
]
operations = [
migrations.CreateModel(
name="SpectacleRevente",
name='SpectacleRevente',
fields=[
(
"id",
models.AutoField(
serialize=False,
primary_key=True,
auto_created=True,
verbose_name="ID",
),
),
(
"date",
models.DateTimeField(
verbose_name="Date de mise en vente",
default=django.utils.timezone.now,
),
),
(
"notif_sent",
models.BooleanField(
verbose_name="Notification envoyée", default=False
),
),
(
"tirage_done",
models.BooleanField(verbose_name="Tirage effectué", default=False),
),
('id', models.AutoField(serialize=False, primary_key=True,
auto_created=True, verbose_name='ID')),
('date', models.DateTimeField(
verbose_name='Date de mise en vente',
default=django.utils.timezone.now)),
('notif_sent', models.BooleanField(
verbose_name='Notification envoyée', default=False)),
('tirage_done', models.BooleanField(
verbose_name='Tirage effectué', default=False)),
],
options={"verbose_name": "Revente"},
options={
'verbose_name': 'Revente',
},
),
migrations.AddField(
model_name="participant",
name="choicesrevente",
field=models.ManyToManyField(
to="bda.Spectacle", related_name="subscribed", blank=True
),
model_name='participant',
name='choicesrevente',
field=models.ManyToManyField(to='bda.Spectacle',
related_name='subscribed',
blank=True),
),
migrations.AddField(
model_name="spectaclerevente",
name="answered_mail",
field=models.ManyToManyField(
to="bda.Participant", related_name="wanted", blank=True
),
model_name='spectaclerevente',
name='answered_mail',
field=models.ManyToManyField(to='bda.Participant',
related_name='wanted',
blank=True),
),
migrations.AddField(
model_name="spectaclerevente",
name="attribution",
field=models.OneToOneField(
to="bda.Attribution", on_delete=models.CASCADE, related_name="revente"
),
model_name='spectaclerevente',
name='attribution',
field=models.OneToOneField(to='bda.Attribution',
on_delete=models.CASCADE,
related_name='revente'),
),
migrations.AddField(
model_name="spectaclerevente",
name="seller",
field=models.ForeignKey(
to="bda.Participant",
on_delete=models.CASCADE,
verbose_name="Vendeur",
related_name="original_shows",
),
model_name='spectaclerevente',
name='seller',
field=models.ForeignKey(to='bda.Participant',
on_delete=models.CASCADE,
verbose_name='Vendeur',
related_name='original_shows'),
),
migrations.AddField(
model_name="spectaclerevente",
name="soldTo",
field=models.ForeignKey(
to="bda.Participant",
on_delete=models.CASCADE,
verbose_name="Vendue à",
null=True,
blank=True,
),
model_name='spectaclerevente',
name='soldTo',
field=models.ForeignKey(to='bda.Participant',
on_delete=models.CASCADE,
verbose_name='Vendue à', null=True,
blank=True),
),
]

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import timedelta
from django.db import migrations, models
from django.db import models, migrations
from django.utils import timezone
from datetime import timedelta
def forwards_func(apps, schema_editor):
@ -12,24 +11,23 @@ def forwards_func(apps, schema_editor):
for revente in SpectacleRevente.objects.all():
is_expired = timezone.now() > revente.date_tirage()
is_direct = (
revente.attribution.spectacle.date >= revente.date
and timezone.now() > revente.date + timedelta(minutes=15)
)
is_direct = (revente.attribution.spectacle.date >= revente.date and
timezone.now() > revente.date + timedelta(minutes=15))
revente.shotgun = is_expired or is_direct
revente.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0009_revente")]
dependencies = [
('bda', '0009_revente'),
]
operations = [
migrations.AddField(
model_name="spectaclerevente",
name="shotgun",
field=models.BooleanField(
default=False, verbose_name="Disponible imm\xe9diatement"
),
model_name='spectaclerevente',
name='shotgun',
field=models.BooleanField(default=False, verbose_name='Disponible imm\xe9diatement'),
),
migrations.RunPython(forwards_func, migrations.RunPython.noop),
]

View file

@ -5,14 +5,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0010_spectaclerevente_shotgun")]
dependencies = [
('bda', '0010_spectaclerevente_shotgun'),
]
operations = [
migrations.AddField(
model_name="tirage",
name="appear_catalogue",
model_name='tirage',
name='appear_catalogue',
field=models.BooleanField(
default=False, verbose_name="Tirage à afficher dans le catalogue"
default=False,
verbose_name='Tirage à afficher dans le catalogue'
),
)
),
]

View file

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [
migrations.RenameField(
model_name="spectaclerevente",
old_name="answered_mail",
new_name="confirmed_entry",
),
migrations.AlterField(
model_name="spectaclerevente",
name="confirmed_entry",
field=models.ManyToManyField(
blank=True, related_name="entered", to="bda.Participant"
),
),
migrations.AddField(
model_name="spectaclerevente",
name="notif_time",
field=models.DateTimeField(
blank=True, verbose_name="Moment d'envoi de la notification", null=True
),
),
]

View file

@ -1,50 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def swap_double_choice(apps, schema_editor):
choices = apps.get_model("bda", "ChoixSpectacle").objects
choices.filter(double_choice="double").update(double_choice="tmp")
choices.filter(double_choice="autoquit").update(double_choice="double")
choices.filter(double_choice="tmp").update(double_choice="autoquit")
class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [
# Temporarily allow an extra "tmp" value for the `double_choice` field
migrations.AlterField(
model_name="choixspectacle",
name="double_choice",
field=models.CharField(
verbose_name="Nombre de places",
max_length=10,
default="1",
choices=[
("tmp", "tmp"),
("1", "1 place"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
],
),
),
migrations.RunPython(swap_double_choice, migrations.RunPython.noop),
migrations.AlterField(
model_name="choixspectacle",
name="double_choice",
field=models.CharField(
verbose_name="Nombre de places",
max_length=10,
default="1",
choices=[
("1", "1 place"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
],
),
),
]

View file

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-05-24 19:23
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
operations = []

View file

@ -1,30 +0,0 @@
# Generated by Django 2.2 on 2019-06-03 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0013_merge_20180524_2123")]
operations = [
migrations.AddField(
model_name="attribution",
name="paid",
field=models.BooleanField(default=False, verbose_name="Payée"),
),
migrations.AddField(
model_name="attribution",
name="paymenttype",
field=models.CharField(
blank=True,
choices=[
("cash", "Cash"),
("cb", "CB"),
("cheque", "Chèque"),
("autre", "Autre"),
],
max_length=6,
verbose_name="Moyen de paiement",
),
),
]

View file

@ -1,36 +0,0 @@
# Generated by Django 2.2 on 2019-06-03 19:30
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db import migrations
def set_attr_payment(apps, schema_editor):
Attribution = apps.get_model("bda", "Attribution")
for attr in Attribution.objects.all():
attr.paid = attr.participant.paid
attr.paymenttype = attr.participant.paymenttype
attr.save()
def set_participant_payment(apps, schema_editor):
Participant = apps.get_model("bda", "Participant")
for part in Participant.objects.all():
attr_set = part.attribution_set
part.paid = attr_set.exists() and not attr_set.filter(paid=False).exists()
try:
# S'il n'y a qu'un seul type de paiement, on le set
part.paymenttype = (
attr_set.values_list("paymenttype", flat=True).distinct().get()
)
# Sinon, whatever
except (ObjectDoesNotExist, MultipleObjectsReturned):
pass
part.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0014_attribution_paid_field")]
operations = [
migrations.RunPython(set_attr_payment, set_participant_payment, atomic=True)
]

View file

@ -1,12 +0,0 @@
# Generated by Django 2.2 on 2019-06-03 19:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("bda", "0015_move_bda_payment")]
operations = [
migrations.RemoveField(model_name="participant", name="paid"),
migrations.RemoveField(model_name="participant", name="paymenttype"),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 2.2 on 2019-09-18 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0016_delete_participant_paid")]
operations = [
migrations.AddField(
model_name="participant",
name="accepte_charte",
field=models.BooleanField(
default=False, verbose_name="A accepté la charte BdA"
),
)
]

View file

@ -1,37 +0,0 @@
# Generated by Django 2.2.12 on 2020-10-21 16:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bda", "0017_participant_accepte_charte"),
]
operations = [
migrations.AlterModelOptions(
name="participant",
options={"ordering": ("-tirage", "user__last_name", "user__first_name")},
),
migrations.AddField(
model_name="tirage",
name="archived",
field=models.BooleanField(default=False, verbose_name="Archivé"),
),
migrations.AlterField(
model_name="participant",
name="tirage",
field=models.ForeignKey(
limit_choices_to={"archived": False},
on_delete=django.db.models.deletion.CASCADE,
to="bda.Tirage",
),
),
migrations.AddConstraint(
model_name="participant",
constraint=models.UniqueConstraint(
fields=("tirage", "user"), name="unique_tirage"
),
),
]

View file

@ -1,22 +1,25 @@
# -*- coding: utf-8 -*-
import calendar
import random
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core import mail
from django.core.mail import EmailMessage, send_mass_mail
from django.db import models
from django.db.models import Count, Exists
from django.template import loader
from django.utils import formats, timezone
from django.db.models import Count
from django.contrib.auth.models import User
from django.conf import settings
from django.utils import timezone, formats
from custommail.models import CustomMail
def get_generic_user():
generic, _ = User.objects.get_or_create(
username="bda_generic",
defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"},
defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"}
)
return generic
@ -28,16 +31,15 @@ class Tirage(models.Model):
tokens = models.TextField("Graine(s) du tirage", blank=True)
active = models.BooleanField("Tirage actif", default=False)
appear_catalogue = models.BooleanField(
"Tirage à afficher dans le catalogue", default=False
"Tirage à afficher dans le catalogue",
default=False
)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
archived = models.BooleanField("Archivé", default=False)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False)
def __str__(self):
return "%s - %s" % (
self.title,
formats.localize(timezone.template_localtime(self.fermeture)),
)
return "%s - %s" % (self.title, formats.localize(
timezone.template_localtime(self.fermeture)))
class Salle(models.Model):
@ -49,7 +51,7 @@ class Salle(models.Model):
class CategorieSpectacle(models.Model):
name = models.CharField("Nom", max_length=100, unique=True)
name = models.CharField('Nom', max_length=100, unique=True)
def __str__(self):
return self.name
@ -61,26 +63,28 @@ class CategorieSpectacle(models.Model):
class Spectacle(models.Model):
title = models.CharField("Titre", max_length=300)
category = models.ForeignKey(
CategorieSpectacle, on_delete=models.CASCADE, blank=True, null=True
CategorieSpectacle, on_delete=models.CASCADE,
blank=True, null=True,
)
date = models.DateTimeField("Date & heure")
location = models.ForeignKey(Salle, on_delete=models.CASCADE)
vips = models.TextField("Personnalités", blank=True)
vips = models.TextField('Personnalités', blank=True)
description = models.TextField("Description", blank=True)
slots_description = models.TextField("Description des places", blank=True)
image = models.ImageField("Image", blank=True, null=True, upload_to="imgs/shows/")
ext_link = models.CharField(
"Lien vers le site du spectacle", blank=True, max_length=500
)
image = models.ImageField('Image', blank=True, null=True,
upload_to='imgs/shows/')
ext_link = models.CharField('Lien vers le site du spectacle', blank=True,
max_length=500)
price = models.FloatField("Prix d'une place")
slots = models.IntegerField("Places")
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
listing = models.BooleanField("Les places sont sur listing")
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True)
rappel_sent = models.DateTimeField("Mail de rappel envoyé", blank=True,
null=True)
class Meta:
verbose_name = "Spectacle"
ordering = ("date", "title")
ordering = ("date", "title",)
def timestamp(self):
return "%d" % calendar.timegm(self.date.utctimetuple())
@ -90,7 +94,7 @@ class Spectacle(models.Model):
self.title,
formats.localize(timezone.template_localtime(self.date)),
self.location,
self.price,
self.price
)
def getImgUrl(self):
@ -99,7 +103,7 @@ class Spectacle(models.Model):
"""
try:
return self.image.url
except Exception:
except:
return None
def send_rappel(self):
@ -109,27 +113,22 @@ class Spectacle(models.Model):
"""
# On récupère la liste des participants + le BdA
members = list(
User.objects.filter(participant__attributions=self)
.annotate(nb_attr=Count("id"))
.order_by()
User.objects
.filter(participant__attributions=self)
.annotate(nb_attr=Count("id")).order_by()
)
bda_generic = get_generic_user()
bda_generic.nb_attr = 1
members.append(bda_generic)
# On écrit un mail personnalisé à chaque participant
mails = [
(
str(self),
loader.render_to_string(
"bda/mails/rappel.txt",
context={"member": member, "nb_attr": member.nb_attr, "show": self},
),
settings.MAIL_DATA["rappels"]["FROM"],
[member.email],
)
datatuple = [(
'bda-rappel',
{'member': member, "nb_attr": member.nb_attr, 'show': self},
settings.MAIL_DATA['rappels']['FROM'],
[member.email])
for member in members
]
send_mass_mail(mails)
send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now()
self.save()
@ -143,8 +142,8 @@ class Spectacle(models.Model):
class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle, on_delete=models.CASCADE)
text = models.TextField("Citation")
author = models.CharField("Auteur", max_length=200)
text = models.TextField('Citation')
author = models.CharField('Auteur', max_length=200)
PAYMENT_TYPES = (
@ -155,231 +154,143 @@ PAYMENT_TYPES = (
)
class Attribution(models.Model):
participant = models.ForeignKey("Participant", on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="attribues"
)
given = models.BooleanField("Donnée", default=False)
paid = models.BooleanField("Payée", default=False)
paymenttype = models.CharField(
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
)
def __str__(self):
return "%s -- %s, %s" % (
self.participant.user,
self.spectacle.title,
self.spectacle.date,
)
class ParticipantPaidQueryset(models.QuerySet):
"""
Un manager qui annote le queryset avec un champ `paid`,
indiquant si un participant a payé toutes ses attributions.
"""
def annotate_paid(self):
# OuterRef permet de se référer à un champ d'un modèle non encore fixé
# Voir:
# https://docs.djangoproject.com/en/2.2/ref/models/expressions/#django.db.models.OuterRef
unpaid = Attribution.objects.filter(
participant=models.OuterRef("pk"), paid=False
)
return self.annotate(paid=~Exists(unpaid))
class Participant(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(
Spectacle, through="ChoixSpectacle", related_name="chosen_by"
)
attributions = models.ManyToManyField(
Spectacle, through="Attribution", related_name="attributed_to"
)
tirage = models.ForeignKey(
Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False}
)
accepte_charte = models.BooleanField("A accepté la charte BdA", default=False)
choicesrevente = models.ManyToManyField(
Spectacle, related_name="subscribed", blank=True
)
objects = ParticipantPaidQueryset.as_manager()
choices = models.ManyToManyField(Spectacle,
through="ChoixSpectacle",
related_name="chosen_by")
attributions = models.ManyToManyField(Spectacle,
through="Attribution",
related_name="attributed_to")
paid = models.BooleanField("A payé", default=False)
paymenttype = models.CharField("Moyen de paiement",
max_length=6, choices=PAYMENT_TYPES,
blank=True)
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed",
blank=True)
def __str__(self):
return "%s - %s" % (self.user, self.tirage.title)
class Meta:
ordering = ("-tirage", "user__last_name", "user__first_name")
constraints = [
models.UniqueConstraint(fields=("tirage", "user"), name="unique_tirage"),
]
DOUBLE_CHOICES = (
("1", "1 place"),
("double", "2 places si possible, 1 sinon"),
("autoquit", "2 places sinon rien"),
("autoquit", "2 places si possible, 1 sinon"),
("double", "2 places sinon rien"),
)
class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="participants"
Spectacle, on_delete=models.CASCADE,
related_name="participants",
)
priority = models.PositiveIntegerField("Priorité")
double_choice = models.CharField(
"Nombre de places", default="1", choices=DOUBLE_CHOICES, max_length=10
)
double_choice = models.CharField("Nombre de places",
default="1", choices=DOUBLE_CHOICES,
max_length=10)
def get_double(self):
return self.double_choice != "1"
double = property(get_double)
def get_autoquit(self):
return self.double_choice == "autoquit"
autoquit = property(get_autoquit)
def __str__(self):
return "Vœux de %s pour %s" % (
self.participant.user.get_full_name(),
self.spectacle.title,
)
self.participant.user.get_full_name(),
self.spectacle.title)
class Meta:
ordering = ("priority",)
unique_together = (("participant", "spectacle"),)
unique_together = (("participant", "spectacle",),)
verbose_name = "voeu"
verbose_name_plural = "voeux"
class Attribution(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE,
related_name="attribues",
)
given = models.BooleanField("Donnée", default=False)
def __str__(self):
return "%s -- %s, %s" % (self.participant.user, self.spectacle.title,
self.spectacle.date)
class SpectacleRevente(models.Model):
attribution = models.OneToOneField(
Attribution, on_delete=models.CASCADE, related_name="revente"
)
date = models.DateTimeField("Date de mise en vente", default=timezone.now)
confirmed_entry = models.ManyToManyField(
Participant, related_name="entered", blank=True
Attribution, on_delete=models.CASCADE,
related_name="revente",
)
date = models.DateTimeField("Date de mise en vente",
default=timezone.now)
answered_mail = models.ManyToManyField(Participant,
related_name="wanted",
blank=True)
seller = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
Participant, on_delete=models.CASCADE,
verbose_name="Vendeur",
related_name="original_shows",
)
soldTo = models.ForeignKey(
Participant,
on_delete=models.CASCADE,
Participant, on_delete=models.CASCADE,
verbose_name="Vendue à",
blank=True,
null=True,
blank=True, null=True,
)
notif_sent = models.BooleanField("Notification envoyée", default=False)
notif_time = models.DateTimeField(
"Moment d'envoi de la notification", blank=True, null=True
)
tirage_done = models.BooleanField("Tirage effectué", default=False)
shotgun = models.BooleanField("Disponible immédiatement", default=False)
####
# Some class attributes
###
# TODO : settings ?
# Temps minimum entre le tirage et le spectacle
min_margin = timedelta(days=5)
# Temps entre la création d'une revente et l'envoi du mail
remorse_time = timedelta(hours=1)
# Temps min/max d'attente avant le tirage
max_wait_time = timedelta(days=3)
min_wait_time = timedelta(days=1)
@property
def real_notif_time(self):
if self.notif_time:
return self.notif_time
else:
return self.date + self.remorse_time
notif_sent = models.BooleanField("Notification envoyée",
default=False)
tirage_done = models.BooleanField("Tirage effectué",
default=False)
shotgun = models.BooleanField("Disponible immédiatement",
default=False)
@property
def date_tirage(self):
"""Renvoie la date du tirage au sort de la revente."""
remaining_time = (
self.attribution.spectacle.date - self.real_notif_time - self.min_margin
)
delay = min(remaining_time, self.max_wait_time)
return self.real_notif_time + delay
@property
def is_urgent(self):
"""
Renvoie True iff la revente doit être mise au shotgun directement.
Plus précisément, on doit avoir min_margin + min_wait_time de marge.
"""
spectacle_date = self.attribution.spectacle.date
return spectacle_date <= timezone.now() + self.min_margin + self.min_wait_time
@property
def can_notif(self):
return timezone.now() >= self.date + self.remorse_time
# L'acheteur doit être connu au plus 12h avant le spectacle
remaining_time = (self.attribution.spectacle.date
- self.date - timedelta(hours=13))
# Au minimum, on attend 2 jours avant le tirage
delay = min(remaining_time, timedelta(days=2))
# Le vendeur a aussi 1h pour changer d'avis
return self.date + delay + timedelta(hours=1)
def __str__(self):
return "%s -- %s" % (self.seller, self.attribution.spectacle.title)
return "%s -- %s" % (self.seller,
self.attribution.spectacle.title)
class Meta:
verbose_name = "Revente"
def reset(self, new_date=timezone.now()):
"""Réinitialise la revente pour permettre une remise sur le marché"""
self.seller = self.attribution.participant
self.date = new_date
self.confirmed_entry.clear()
self.soldTo = None
self.notif_sent = False
self.notif_time = None
self.tirage_done = False
self.shotgun = False
self.save()
def send_notif(self):
"""
Envoie une notification pour indiquer la mise en vente d'une place sur
BdA-Revente à tous les intéressés.
"""
inscrits = self.attribution.spectacle.subscribed.select_related("user")
mails = [
(
"BdA-Revente : {}".format(self.attribution.spectacle.title),
loader.render_to_string(
"bda/mails/revente-new.txt",
context={
"member": participant.user,
"show": self.attribution.spectacle,
"revente": self,
"site": Site.objects.get_current(),
},
),
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
inscrits = self.attribution.spectacle.subscribed.select_related('user')
datatuple = [(
'bda-revente',
{
'member': participant.user,
'show': self.attribution.spectacle,
'revente': self,
'site': Site.objects.get_current()
},
settings.MAIL_DATA['revente']['FROM'],
[participant.user.email])
for participant in inscrits
]
send_mass_mail(mails)
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
self.save()
def mail_shotgun(self):
@ -387,104 +298,90 @@ class SpectacleRevente(models.Model):
Envoie un mail à toutes les personnes intéréssées par le spectacle pour
leur indiquer qu'il est désormais disponible au shotgun.
"""
inscrits = self.attribution.spectacle.subscribed.select_related("user")
mails = [
(
"BdA-Revente : {}".format(self.attribution.spectacle.title),
loader.render_to_string(
"bda/mails/revente-shotgun.txt",
context={
"member": participant.user,
"show": self.attribution.spectacle,
"site": Site.objects.get_current(),
},
),
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
inscrits = self.attribution.spectacle.subscribed.select_related('user')
datatuple = [(
'bda-shotgun',
{
'member': participant.user,
'show': self.attribution.spectacle,
'site': Site.objects.get_current(),
},
settings.MAIL_DATA['revente']['FROM'],
[participant.user.email])
for participant in inscrits
]
send_mass_mail(mails)
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde
self.tirage_done = True
self.shotgun = True
self.save()
def tirage(self, send_mails=True):
def tirage(self):
"""
Lance le tirage au sort associé à la revente. Un gagnant est choisi
parmis les personnes intéressées par le spectacle. Les personnes sont
ensuites prévenues par mail du résultat du tirage.
"""
inscrits = list(self.confirmed_entry.all())
inscrits = list(self.answered_mail.all())
spectacle = self.attribution.spectacle
seller = self.seller
winner = None
if inscrits:
# Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits)
self.soldTo = winner
if send_mails:
mails = []
context = {
"acheteur": winner.user,
"vendeur": seller.user,
"show": spectacle,
}
mails = []
subject = "BdA-Revente : {}".format(spectacle.title)
context = {
'acheteur': winner.user,
'vendeur': seller.user,
'show': spectacle,
}
mails.append(
EmailMessage(
subject=subject,
body=loader.render_to_string(
"bda/mails/revente-tirage-winner.txt",
context=context,
),
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[winner.user.email],
)
c_mails_qs = CustomMail.objects.filter(shortname__in=[
'bda-revente-winner', 'bda-revente-loser',
'bda-revente-seller',
])
c_mails = {cm.shortname: cm for cm in c_mails_qs}
mails.append(
c_mails['bda-revente-winner'].get_message(
context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[winner.user.email],
)
mails.append(
EmailMessage(
subject=subject,
body=loader.render_to_string(
"bda/mails/revente-tirage-seller.txt",
context=context,
),
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[seller.user.email],
reply_to=[winner.user.email],
),
)
mails.append(
c_mails['bda-revente-seller'].get_message(
context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[seller.user.email],
reply_to=[winner.user.email],
)
)
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context["acheteur"] = inscrit.user
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit != winner:
new_context = dict(context)
new_context['acheteur'] = inscrit.user
mails.append(
EmailMessage(
subject=subject,
body=loader.render_to_string(
"bda/mails/revente-tirage-loser.txt",
context=new_context,
),
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[inscrit.user.email],
),
mails.append(
c_mails['bda-revente-loser'].get_message(
new_context,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[inscrit.user.email],
)
)
mail_conn = mail.get_connection()
mail_conn.send_messages(mails)
mail_conn = mail.get_connection()
mail_conn.send_messages(mails)
# Si personne ne veut de la place, elle part au shotgun
else:
self.shotgun = True
self.tirage_done = True
self.save()
return winner

View file

@ -1,125 +0,0 @@
form#tokenform {
text-align: center;
font-size: 2em;
}
label {
margin-right: 10px;
vertical-align: top;
}
form#tokenform textarea {
font-size: 2em;
width: 350px;
height: 200px;
font-family: 'Droif Serif', serif;
}
/* wft ?
input {
width: 400px;
font-size: 2em;
}*/
ul.losers {
display: inline;
margin: 0;
padding: 0;
}
ul.losers li {
display: inline;
}
span.details {
font-size: 0.7em;
}
td {
border: 0px solid black;
padding: 2px;
}
.attribresult {
margin: 10px 0px;
}
.spectacle-passe {
opacity:0.5;
}
/** JQuery-Confirm box **/
.jconfirm .jconfirm-bg {
background-color: rgb(0,0,0,0.6) !important;
}
.jconfirm .jconfirm-box {
padding:0;
border-radius:0 !important;
font-family:Roboto;
}
.jconfirm .jconfirm-box .content-pane {
border-bottom:1px solid #ddd;
margin: 0px !important;
}
.jconfirm .jconfirm-box .content {
padding: 5px;
}
.jconfirm .jconfirm-box .content-pane {
border-bottom:1px solid #ddd;
margin: 0px !important;
}
.jconfirm .jconfirm-box .content {
padding: 10px;
}
.jconfirm .jconfirm-box .content a,
.jconfirm .jconfirm-box .content a:hover {
color: #D81138;
font-weight: bold;
}
.jconfirm .jconfirm-box .buttons {
margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */
padding:0;
height:40px;
}
.jconfirm .jconfirm-box .buttons button {
min-width:40px;
height:100%;
margin:0;
margin:0 !important;
border-radius: 0 !important;
}
.jconfirm .jconfirm-box .buttons button:first-child:focus,
.jconfirm .jconfirm-box .buttons button:first-child:hover {
color:#FFF !important;
background:forestgreen !important;
}
.jconfirm .jconfirm-box .buttons button:nth-child(2):focus,
.jconfirm .jconfirm-box .buttons button:nth-child(2):hover {
color:#FFF !important;
background:#D93A32 !important;
}
.jconfirm .jconfirm-box div.title-c .title {
display: block;
padding:0 15px;
height:40px;
line-height:40px;
font-family:Dosis;
font-size:20px;
font-weight:bold;
color:#FFF;
background-color:rgb(222, 130, 107);
}

48
bda/static/css/bda.css Normal file
View file

@ -0,0 +1,48 @@
form#tokenform {
text-align: center;
font-size: 2em;
}
label {
margin-right: 10px;
vertical-align: top;
}
form#tokenform textarea {
font-size: 2em;
width: 350px;
height: 200px;
font-family: 'Droif Serif', serif;
}
/* wft ?
input {
width: 400px;
font-size: 2em;
}*/
ul.losers {
display: inline;
margin: 0;
padding: 0;
}
ul.losers li {
display: inline;
}
span.details {
font-size: 0.7em;
}
td {
border: 0px solid black;
padding: 2px;
}
.attribresult {
margin: 10px 0px;
}
.spectacle-passe {
opacity:0.5;
}

Binary file not shown.

View file

@ -2,7 +2,7 @@
{% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}

View file

@ -0,0 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2><strong>Nope</strong></h2>
<p>Avant de revendre des places, il faut aller les payer !</p>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Places disponibles immédiatement</h2>
{% if shotgun %}
<ul class="list-unstyled">
{% for spectacle in shotgun %}
<li><a href="{% url "bda-buy-revente" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %}
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>
{% endif %}
{% endblock %}

View file

@ -6,7 +6,7 @@
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
<p>Si personne n'était intéressé, elle est maintenant disponible
<a href="{% url "bda-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
{% else %}
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
{% endif %}

View file

@ -42,6 +42,9 @@
Total&nbsp;: {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
</span>
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
</script>
<script type="text/javascript">
$(function(){
$("table.etat-bda").stupidtable();

View file

@ -1 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=attribution.spectacle %}

View file

@ -1,4 +0,0 @@
<tr>
<td><input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}></td>
{{ widget.label }}
</tr>

View file

@ -1 +0,0 @@
<td data-sort-value="{{ revente.date_tirage | date:"U" }}">{{ revente.date_tirage }}</td>

View file

@ -1,3 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% with user=revente.seller.user %} <td>{{user.first_name}} {{user.last_name}}</td> {% endwith%}
{% include 'bda/forms/date_tirage.html' %}

View file

@ -1,2 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% include 'bda/forms/date_tirage.html' %}

View file

@ -1,4 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% with user=revente.soldTo.user %}
<td><a href="mailto:{{ user.email }}">{{user.first_name}} {{user.last_name}}</a></td>
{% endwith %}

View file

@ -1,4 +0,0 @@
<td>{{ spectacle.title }}</td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">{{ spectacle.price |floatformat }}€</td>

View file

@ -14,7 +14,7 @@
</tr></thead>
<tbody class="bda_formset_content">
{% endif %}
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
{% for field in form.visible_fields %}
{% if field.name != "DELETE" and field.name != "priority" %}
<td class="bda-field-{{ field.name }}">

View file

@ -2,12 +2,10 @@
{% load staticfiles %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script>
<script type="text/javascript" src="{% static "vendor/jquery/jquery-confirm.js" %}"></script>
<script type="text/javascript" src="{% static 'gestioncof/vendor/jquery.ui.touch-punch.min.js' %}" ></script>
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-confirm.css' %}">
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-ui.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static 'bda/css/bda.css' %}" />
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}
@ -29,14 +27,6 @@ var django = {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
// Cloning <select> element doesn't properly propagate the default
// selected <option>, so we set it manually.
newElement.find('select').each(function (index, select) {
var defaultValue = $(select).find('option[selected]').val();
if (typeof defaultValue !== 'undefined') {
$(select).val(defaultValue);
}
});
total++;
$('#id_' + type + '-TOTAL_FORMS').val(total);
$(selector).after(newElement);
@ -54,11 +44,6 @@ var django = {
} else {
deleteInput.attr("checked", true);
}
} else {
// Reset the default values
var selects = $(form).find("select");
$(selects[0]).val("");
$(selects[1]).val("1");
}
// callback
});
@ -120,50 +105,11 @@ var django = {
});
</script>
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
<input type="submit" class="btn btn-primary" id="bda-inscr" value="Enregistrer" />
<input type="submit" class="btn btn-primary" value="Enregistrer" />
</div>
<p class="footnotes">
<sup>1</sup>: cette liste de v&oelig;ux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque v&oelig;u.<br />
</p>
</div>
</form>
{% if not charte %}
<script>
(function ($) {
var charte_ok = false ;
function link_charte() {
$.confirm({
title: 'Charte du BdA',
columnClass: 'col-md-6 col-md-offset-3',
content: `
<div>
En vous inscrivant à ce tirage du Bureau des Arts, vous vous engagez à \
respecter la charte du BdA:</br> \
<a target="_blank" href='https://bda.ens.fr/lequipe/charte-bda/'>https://bda.ens.fr/lequipe/charte-bda/</a>
</div>`,
backgroundDismiss: true,
opacity: 1,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirmButton: '<span class="glyphicon glyphicon-ok"></span>',
cancelButton: '<span class="glyphicon glyphicon-remove"></span>',
confirm: function() {
charte_ok = true ;
$("#bda_form").submit();
},
});
}
$(document).ready(function($) {
$("#bda_form").submit(function(e) {
if (!charte_ok) {
e.preventDefault();
link_charte();
}
})
})
})(django.jQuery);
</script>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,33 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
{% csrf_token %}
<div class="form-group">
<h3>Spectacles</h3>
<br/>
<button type="button" class="btn btn-primary" onClick="select(true)">Tout sélectionner</button>
<button type="button" class="btn btn-primary" onClick="select(false)">Tout désélectionner</button>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{checkbox}}</li>
{%endfor%}
</ul>
</div>
</div>
<input type="submit" class="btn btn-primary" value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -26,6 +26,13 @@
<hr \>
<p>
<em>Note :</em> le template de ce mail peut être modifié à
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
</p>
<hr \>
<h3>Forme des mails</h3>
<h4>Une seule place</h4>

View file

@ -1,10 +0,0 @@
Cher-e {{ member.first_name }},
Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as
obtenu aucune place.
Nous proposons cependant de nombreuses offres hors-tirage tout au long de
l'année, et nous t'invitons à nous contacter si l'une d'entre elles
t'intéresse !
--
Le Bureau des Arts

View file

@ -1,31 +0,0 @@
Cher-e {{ member.first_name }},
Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e
pour les spectacles suivants :
{% for place in places %}
- 1 place pour {{ place }}{% endfor %}
*Paiement*
L'intégralité de ces places de spectacles est à régler dès maintenant et AVANT
vendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi
entre 12h et 14h, et entre 18h et 20h). Des facilités de paiement sont bien
évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement,
ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas
venir payer au bureau, merci de nous contacter par mail.
*Mode de retrait des places*
Au moment du paiement, certaines places vous seront remises directement,
d'autres seront à récupérer au cours de l'année, d'autres encore seront
nominatives et à retirer le soir même dans les théâtres correspondants.
Pour chaque spectacle, vous recevrez un mail quelques jours avant la
représentation vous indiquant le mode de retrait.
Nous vous rappelons que l'obtention de places du BdA vous engage à
respecter les règles de fonctionnement :
https://bda.ens.fr/lequipe/charte-bda/
Un système de revente des places via les mails BdA-revente est disponible
directement sur votre compte GestioCOF.
En vous souhaitant de très beaux spectacles tout au long de l'année,
--
Le Bureau des Arts

View file

@ -1,23 +0,0 @@
Bonjour {{ member.first_name }},
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
{% if nb_attr == 2 %}
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
ces places sont strictement réservées aux personnes de moins de 28 ans.
{% endif %}
{% if show.listing %}Pour ce spectacle, tu as reçu {{ nb_attr|pluralize:"une place,des places" }} sur
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
pour {{ nb_attr|pluralize:"la,les" }} retirer.
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
été distribués au burô.
{% endif %}
Si tu ne peux plus assister à cette représentation, tu peux
revendre ta place via BdA-revente, accessible directement sur
GestioCOF (lien "revendre une place du premier tirage" sur la page
d'accueil https://www.cof.ens.fr/gestion/).
En te souhaitant un excellent spectacle,
--
Le Bureau des Arts

View file

@ -1,12 +0,0 @@
Bonjour {{ member.first_name }}
Une place pour le spectacle {{ show.title }} ({{ show.date }})
a été postée sur BdA-Revente.
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
sur ce lien : https://{{ site }}{% url "bda-revente-confirm" revente.id %}.
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
un tirage au sort le {{ revente.date_tirage|date:"DATE_FORMAT" }}.
Chaleureusement,
Le BdA

View file

@ -1,13 +0,0 @@
Bonjour {{ vendeur.first_name }},
Tu tes bien inscrit·e pour revendre une place pour {{ show.title }}.
{% with revente.date_tirage as time %}
Le tirage au sort entre tout·e·s les racheteuse·eur·s potentiel·le·s aura lieu
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
Si personne ne sest inscrit pour racheter la place, celle-ci apparaîtra parmi
les « Places disponibles immédiatement à la revente » sur GestioCOF.
{% endwith %}
Bonne revente !
Le Bureau des Arts

View file

@ -1,6 +0,0 @@
Bonjour {{ vendeur.first_name }} !
Je souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
Contacte-moi si tu es toujours intéressé·e !
{{ acheteur.get_full_name }} ({{ acheteur.email }})

View file

@ -1,11 +0,0 @@
Bonjour {{ member.first_name }}
Une place pour le spectacle {{ show.title }} ({{ show.date }})
a été postée sur BdA-Revente.
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour
cette place : elle est disponible immédiatement à l'adresse
https://{{ site }}{% url "bda-revente-buy" show.id %}, à la disposition de tous.
Chaleureusement,
Le BdA

View file

@ -1,9 +0,0 @@
Bonjour {{ acheteur.first_name }},
Tu t'étais inscrit·e pour la revente de la place de {{ vendeur.get_full_name }}
pour {{ show.title }}.
Malheureusement, une autre personne a été tirée au sort pour racheter la place.
Tu pourras certainement retenter ta chance pour une autre revente !
À très bientôt,
Le Bureau des Arts

View file

@ -1,7 +0,0 @@
Bonjour {{ vendeur.first_name }},
La personne tirée au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.
Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail.
Chaleureusement,
Le BdA

View file

@ -1,7 +0,0 @@
Bonjour {{ acheteur.first_name }},
Tu as été tiré·e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
Tu peux contacter le/la vendeur·se à l'adresse {{ vendeur.email }}.
Chaleureusement,
Le BdA

View file

@ -16,7 +16,7 @@
<tbody>
{% for participant in participants %}
<tr>
<td data-sort-value="{{ participant.name}}">{{participant.name}}</td>
<td data-sort-value="{{ participan.name}}">{{participant.name}}</td>
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
<td data-sort-value="{{participant.email}}">{{participant.email}}</td>
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
@ -38,7 +38,7 @@
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
<div>
<div>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participant⋅e⋅s</button>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
<pre id="export-mails" style="display:none">{% spaceless %}
{% for participant in participants %}{{ participant.email }}, {% endfor %}
{% endspaceless %}</pre>
@ -56,7 +56,9 @@
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
</div>
<script type="text/javascript">
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script>
function toggle(id) {
var pre = document.getElementById(id) ;
pre.style.display = pre.style.display == "none" ? "block" : "none" ;

View file

@ -10,14 +10,13 @@
<td>{{place.spectacle.location}}</td>
<td>{{place.spectacle.date}}</td>
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
<td>{% if place.spectacle.listing %}sur listing{% else %}place physique{% endif %}</td>
</tr>
{% endfor %}
</table>
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
<br/>
<p>Ne manque pas un spectacle avec le
<a href="{% url "calendar" %}">calendrier
<a href="{% url "gestioncof.views.calendar" %}">calendrier
automatique&#8239;!</a></p>
{% else %}
<h3>Vous n'avez aucune place :(</h3>

View file

@ -1,121 +0,0 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Gestion des places que je revends</h2>
{% if resell_exists %}
<br />
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les places que vous souhaitez revendre, et validez. Vous aurez
ensuite 1h pour changer d'avis avant que la revente soit confirmée et
que les notifications soient envoyées aux intéressé·e·s.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Date</th>
<th data-sort="int">Prix</th>
</tr>
</thead>
<tbody>
{% for checkbox in resellform.attributions %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
<hr />
{% endif %}
{% if annul_exists %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
</form>
<hr />
{% endif %}
{% if sold_exists %}
<h3>Places revendues</h3>
<form action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Pour chaque revente, vous devez soit l'annuler soit la confirmer pour
transférer la place la place à la personne tirée au sort.
L'annulation sert par exemple à pouvoir remettre la place en jeu si
vous ne parvenez pas à entrer en contact avec la personne tirée au
sort.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue à</th>
</tr>
</thead>
<tbody>
{% for checkbox in soldform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_exists and not annul_exists and not sold_exists %}
<p>Plus de reventes possibles !</p>
{% endif %}
<script language="JavaScript">
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -1,29 +0,0 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Places disponibles immédiatement</h2>
{% if spectacles %}
<table class="table table-striped stupidtable" id="bda-shotgun">
<thead>
<tr>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th></th>
</tr>
</thead>
<tbody>
{% for spectacle in spectacles %}
<tr>
{% include "bda/forms/spectacle_label_table.html" with spectacle=spectacle %}
<td class="button"><a role="button" class="btn btn-primary" href="{% url 'bda-revente-buy' spectacle.id %}">Racheter</a>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>
{% endif %}
{% endblock %}

View file

@ -1,64 +0,0 @@
{% extends "base_title.html" %}
{% load staticfiles%}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les spectacles pour lesquels vous souhaitez recevoir une
notification quand une place est disponible en revente. <br />
Lorsque vous validez vos choix, si un tirage au sort est en cours pour
un des spectacles que vous avez sélectionné, vous serez automatiquement
inscrit à ce tirage.
</div>
<br />
{% csrf_token %}
<div class="form-group">
<button type="button"
class="btn btn-primary"
onClick="select(true)">Tout sélectionner</button>
<button type="button"
class="btn btn-primary"
onClick="select(false)">Tout désélectionner</button>
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
</tr>
</thead>
<tbody>
{% for checkbox in form.spectacles %}{{ checkbox }}{% endfor %}
</tbody>
</table>
</div>
<input type="submit"
class="btn btn-primary"
value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length; i < n; i++) {
checkboxes[i].checked = check;
}
}
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -1,99 +0,0 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Tirages au sort de reventes</h2>
{% if annul_exists %}
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Voici la liste des reventes auxquelles vous êtes inscrit·e ; si vous ne souhaitez plus participer au tirage au sort vous pouvez vous en désister.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue par</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="annul"
value="Se désister des tirages sélectionnés">
</div>
</form>
{% endif %}
<hr />
{% if sub_exists %}
<h3>Tirages en cours</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous inscrire aux tirages en cours suivants.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue par</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in subform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="subscribe"
value="S'inscrire aux tirages sélectionnés">
</div>
</form>
{% endif %}
{% if not annul_exists and not sub_exists %}
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Aucune revente n'est active pour le moment !
</div>
{% endif %}
<script language="JavaScript">
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Revente de place</h2>
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
{% if resellform.attributions %}
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{{resellform|bootstrap}}
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
{% endif %}
<br>
{% if annul_attributions or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% csrf_token %}
<div class='form-group'>
<div class='multiple-checkbox'>
<ul>
{% for attrib in annul_attributions %}
<li>{{attrib.tag}} {{attrib.choice_label}}</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{attrib.spectacle}}
</li>
{% endfor %}
{% if annul_attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
{% endif %}
<br>
{% if sold_attributions %}
<h3>Places revendues</h3>
<form action="" method="post">
{% csrf_token %}
{{soldform|bootstrap}}
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}
<p>Plus de reventes possibles !</p>
{% endif %}
{% endwith %}
{% endblock %}

View file

@ -0,0 +1,113 @@
{% load staticfiles %}
<!doctype html>
<html>
<head>
<base target="_parent"/>
<style>
@font-face {
font-family: josefinsans;
src: url({% static "fonts/josefinsans.ttf" %});
}
*::-moz-selection {
background: #B0B0B0;
}
*::selection {
background: #B0B0B0;
}
.descTable{
width: 100%;
margin: 0 auto 1em;
border-bottom: 2px solid;
border-collapse: collapse;
border-spacing: 0;
font-size: 14px;
line-height: 2;
max-width: 100%;
background-color: transparent;
font-family: 'josefinsans', 'Arial';
font-weight: 700;
color: #5a5a5a;
}
img{
max-width: 100%;
}
</style>
<meta charset="utf8" />
</head>
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<body>
{% for show in shows %}
<table class="descTable">
<thead>
<tr>
<th colspan="2"><p style="text-align:center;font-size:22px;">{{ show.title }}</p></th>
</tr>
</thead>
<tbody>
<tr>
<td><p style="text-align: left;">{{ show.location }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.category }}</p></td>
</tr>
<tr>
<td><p style="text-align: left;">{{ show.date|date:"l j F Y - H\hi" }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %} - {{ show.price }} euro{{ show.price|pluralize}}</p></td>
</tr>
{% if show.vips %}
<tr>
<td colspan="2"><p style="text-align: justify;">{{ show.vips }}</p></td>
</tr>
{% endif %}
<tr>
<td colspan="2">
<p style="text-align: justify;">{{ show.description }}</p>
{% for quote in show.quote_set.all %}
<p style="text-align:center; font-style: italic;">«{{ quote.text }}»{% if quote.author %} - {{ quote.author }}{% endif %}</p>
{% endfor %}
</td>
</tr>
{% if show.image %}
<tr>
<td colspan="2"><p style="text-align:center;"><a href="{{ show.ext_link }}"><img class="imgDesc" style="display: inline;" src="{{ MEDIA_URL }}{{ show.image }}" alt="{{ show.title }}"></a></p></td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
<script>
// Correction de la taille des images
/*$(document).ready(function() {
$(".descTable").each(function() {
$(this).width($("body").width());
});
$(".imgDesc").on("load", function() {
// Dimensions
origHeight = 500; // Hauteur souhaitée
w = $(this).width();
h = $(this).height();
r = w/h; // Ratio de l'image
maxWidth = $("body").width();
if (r * origHeight > maxWidth)
{
$(this).width(maxWidth);
$(this).height(maxWidth/r);
}
else
{
$(this).width(r * origHeight);
$(this).height(origHeight);
}
});
});*/
</script>
</body>
</html>

View file

@ -2,7 +2,7 @@
{% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}
@ -17,11 +17,11 @@
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="float">Prix</th>
</tr>
</tr>
</thead>
<tbody>
{% for spectacle in object_list %}
<tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
<td data-sort-value="{{ spectacle.timestamp }}"">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
@ -32,7 +32,9 @@
{% endfor %}
</tbody>
</table>
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
</script>
<script type="text/javascript">
$(function(){
$("table.etat-bda").stupidtable();
@ -49,5 +51,6 @@
<h3> Exports </h3>
<ul>
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
<li><a href="{% url 'bda-descriptions' tirage_id %}">Lien vers les descriptions des spectacles, à utiliser dans une page wordpress</a>
</ul>
{% endblock %}

View file

@ -1,65 +0,0 @@
from django.utils import timezone
from shared.tests.mixins import ViewTestCaseMixin
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
from .utils import create_user
class BdAViewTestCaseMixin(ViewTestCaseMixin):
def get_users_base(self):
return {
"bda_other": create_user(username="bda_other"),
"bda_member": create_user(username="bda_member", is_cof=True),
"bda_staff": create_user(username="bda_staff", is_cof=True, is_buro=True),
}
class BdATestHelpers:
bda_testdata = False
def setUp(self):
super().setUp()
if self.bda_testdata:
self.load_bda_testdata()
def load_bda_testdata(self):
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
self.show1 = Spectacle.objects.create(
title="foo",
date=timezone.now(),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.show2 = Spectacle.objects.create(
title="bar",
date=timezone.now(),
location=self.location,
price=1,
slots=142,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.show3 = Spectacle.objects.create(
title="baz",
date=timezone.now(),
location=self.location,
price=2,
slots=242,
tirage=self.tirage,
listing=False,
category=self.category,
)

View file

@ -7,31 +7,28 @@ from django.test import TestCase
from django.utils import timezone
from bda.models import (
Attribution,
Participant,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
Attribution, Participant, Salle, Spectacle, SpectacleRevente, Tirage,
)
User = get_user_model()
class SpectacleReventeTests(TestCase):
fixtures = ['gestioncof/management/data/custommail.json']
def setUp(self):
now = timezone.now()
self.t = Tirage.objects.create(
title="Tirage",
title='Tirage',
ouverture=now - timedelta(days=7),
fermeture=now - timedelta(days=3),
active=True,
)
self.s = Spectacle.objects.create(
title="Spectacle",
title='Spectacle',
date=now + timedelta(days=20),
location=Salle.objects.create(name="Salle", address="Address"),
location=Salle.objects.create(name='Salle', address='Address'),
price=10.5,
slots=5,
tirage=self.t,
@ -39,37 +36,40 @@ class SpectacleReventeTests(TestCase):
)
self.seller = Participant.objects.create(
user=User.objects.create(username="seller", email="seller@mail.net"),
user=User.objects.create(
username='seller', email='seller@mail.net'),
tirage=self.t,
)
self.p1 = Participant.objects.create(
user=User.objects.create(username="part1", email="part1@mail.net"),
user=User.objects.create(username='part1', email='part1@mail.net'),
tirage=self.t,
)
self.p2 = Participant.objects.create(
user=User.objects.create(username="part2", email="part2@mail.net"),
user=User.objects.create(username='part2', email='part2@mail.net'),
tirage=self.t,
)
self.p3 = Participant.objects.create(
user=User.objects.create(username="part3", email="part3@mail.net"),
user=User.objects.create(username='part3', email='part3@mail.net'),
tirage=self.t,
)
self.attr = Attribution.objects.create(
participant=self.seller, spectacle=self.s
participant=self.seller,
spectacle=self.s,
)
self.rev = SpectacleRevente.objects.create(
attribution=self.attr, seller=self.seller
attribution=self.attr,
seller=self.seller,
)
def test_tirage(self):
revente = self.rev
wanted_by = [self.p1, self.p2, self.p3]
revente.confirmed_entry.set(wanted_by)
revente.answered_mail = wanted_by
with mock.patch("bda.models.random.choice") as mc:
with mock.patch('bda.models.random.choice') as mc:
# Set winner to self.p1.
mc.return_value = self.p1
@ -87,14 +87,14 @@ class SpectacleReventeTests(TestCase):
self.assertEqual(len(mails), 4)
m_seller = mails["seller@mail.net"]
self.assertListEqual(m_seller.to, ["seller@mail.net"])
self.assertListEqual(m_seller.reply_to, ["part1@mail.net"])
m_seller = mails['seller@mail.net']
self.assertListEqual(m_seller.to, ['seller@mail.net'])
self.assertListEqual(m_seller.reply_to, ['part1@mail.net'])
m_winner = mails["part1@mail.net"]
self.assertListEqual(m_winner.to, ["part1@mail.net"])
m_winner = mails['part1@mail.net']
self.assertListEqual(m_winner.to, ['part1@mail.net'])
self.assertCountEqual(
[mails["part2@mail.net"].to, mails["part3@mail.net"].to],
[["part2@mail.net"], ["part3@mail.net"]],
[mails['part2@mail.net'].to, mails['part3@mail.net'].to],
[['part2@mail.net'], ['part3@mail.net']],
)

View file

@ -1,79 +0,0 @@
from datetime import timedelta
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone
from bda.models import (
Attribution,
CategorieSpectacle,
Participant,
Salle,
Spectacle,
SpectacleRevente,
Tirage,
)
class TestModels(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="Tirage test",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
self.spectacle_soon = Spectacle.objects.create(
title="foo",
date=timezone.now() + timedelta(days=1),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.spectacle_later = Spectacle.objects.create(
title="bar",
date=timezone.now() + timedelta(days=30),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
user_buyer = User.objects.create_user(
username="bda_buyer", password="testbuyer"
)
user_seller = User.objects.create_user(
username="bda_seller", password="testseller"
)
self.buyer = Participant.objects.create(user=user_buyer, tirage=self.tirage)
self.seller = Participant.objects.create(user=user_seller, tirage=self.tirage)
self.attr_soon = Attribution.objects.create(
participant=self.seller, spectacle=self.spectacle_soon
)
self.attr_later = Attribution.objects.create(
participant=self.seller, spectacle=self.spectacle_later
)
self.revente_soon = SpectacleRevente.objects.create(
seller=self.seller, attribution=self.attr_soon
)
self.revente_later = SpectacleRevente.objects.create(
seller=self.seller, attribution=self.attr_later
)
def test_urgent(self):
self.assertTrue(self.revente_soon.is_urgent)
self.assertFalse(self.revente_later.is_urgent)
def test_tirage(self):
self.revente_soon.confirmed_entry.add(self.buyer)
self.assertEqual(self.revente_soon.tirage(send_mails=False), self.buyer)
self.assertIsNone(self.revente_later.tirage(send_mails=False))

View file

@ -1,319 +1,98 @@
import json
from datetime import timedelta
from unittest import mock
from django.contrib.auth import get_user_model
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import formats, timezone
from django.contrib.auth.models import User
from django.test import TestCase, Client
from django.utils import timezone
from ..models import Participant, Tirage
from .mixins import BdATestHelpers, BdAViewTestCaseMixin
User = get_user_model()
from bda.models import Tirage, Spectacle, Salle, CategorieSpectacle
class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-tirage-inscription"
http_methods = ["GET", "POST"]
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/inscription/{}".format(self.tirage.id)
def test_get_opened(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertFalse(resp.context["messages"])
def test_get_closed_future(self):
self.tirage.ouverture = timezone.now() + timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=2)
self.tirage.save()
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Le tirage n'est pas encore ouvert : ouverture le {}".format(
formats.localize(timezone.template_localtime(self.tirage.ouverture))
class TestBdAViews(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
Spectacle.objects.bulk_create([
Spectacle(
title="foo", date=timezone.now(), location=self.location,
price=0, slots=42, tirage=self.tirage, listing=False,
category=self.category
),
[str(msg) for msg in resp.context["messages"]],
)
def test_get_closed_past(self):
self.tirage.ouverture = timezone.now() - timedelta(days=2)
self.tirage.fermeture = timezone.now() - timedelta(days=1)
self.tirage.save()
resp = self.client.get(self.url)
self.assertEqual(resp.status_code, 200)
self.assertIn(
" C'est fini : tirage au sort dans la journée !",
[str(msg) for msg in resp.context["messages"]],
)
def get_base_post_data(self):
return {
"choixspectacle_set-TOTAL_FORMS": "3",
"choixspectacle_set-INITIAL_FORMS": "0",
"choixspectacle_set-MIN_NUM_FORMS": "0",
"choixspectacle_set-MAX_NUM_FORMS": "1000",
}
base_post_data = property(get_base_post_data)
def test_post(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
data = dict(
self.base_post_data,
**{
"choixspectacle_set-TOTAL_FORMS": "2",
"choixspectacle_set-0-id": "",
"choixspectacle_set-0-participant": "",
"choixspectacle_set-0-spectacle": str(self.show1.pk),
"choixspectacle_set-0-double_choice": "1",
"choixspectacle_set-0-priority": "2",
"choixspectacle_set-1-id": "",
"choixspectacle_set-1-participant": "",
"choixspectacle_set-1-spectacle": str(self.show2.pk),
"choixspectacle_set-1-double_choice": "autoquit",
"choixspectacle_set-1-priority": "1",
}
)
resp = self.client.post(self.url, data)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Votre inscription a été mise à jour avec succès !",
[str(msg) for msg in resp.context["messages"]],
)
participant = Participant.objects.get(
user=self.users["bda_member"], tirage=self.tirage
)
self.assertSetEqual(
set(
participant.choixspectacle_set.values_list(
"priority", "spectacle_id", "double_choice"
)
Spectacle(
title="bar", date=timezone.now(), location=self.location,
price=1, slots=142, tirage=self.tirage, listing=False,
category=self.category
),
{(1, self.show2.pk, "autoquit"), (2, self.show1.pk, "1")},
Spectacle(
title="baz", date=timezone.now(), location=self.location,
price=2, slots=242, tirage=self.tirage, listing=False,
category=self.category
),
])
self.bda_user = User.objects.create_user(
username="bda_user", password="bda4ever"
)
self.bda_user.profile.is_cof = True
self.bda_user.profile.is_buro = True
self.bda_user.profile.save()
def test_post_state_changed(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
def bda_participants(self):
"""The BdA participants views can be queried"""
client = Client()
show = self.tirage.spectacle_set.first()
data = {"dbstate": "different"}
resp = self.client.post(self.url, data)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Impossible d'enregistrer vos modifications : vous avez apporté d'autres "
"modifications entre temps.",
[str(msg) for msg in resp.context["messages"]],
client.login(self.bda_user.username, "bda4ever")
tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id))
show_resp = client.get(
"/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
)
reminder_url = "/bda/mails-rappel/{}".format(show.id)
reminder_get_resp = client.get(reminder_url)
reminder_post_resp = client.post(reminder_url)
self.assertEqual(200, tirage_resp.status_code)
self.assertEqual(200, show_resp.status_code)
self.assertEqual(200, reminder_get_resp.status_code)
self.assertEqual(200, reminder_post_resp.status_code)
def test_catalogue(self):
"""Test the catalogue JSON API"""
client = Client()
class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-places-attribuees"
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/places/{}".format(self.tirage.id)
class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-etat-places"
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/etat-places/{}".format(self.tirage.id)
class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-tirage"
http_methods = ["GET", "POST"]
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/tirage/{}".format(self.tirage.id)
def test_perform_tirage_disabled(self):
# Cannot be performed if disabled
self.tirage.enable_do_tirage = False
self.tirage.save()
resp = self.client.get(self.url)
self.assertTemplateUsed(resp, "tirage-failed.html")
def test_perform_tirage_opened_registrations(self):
# Cannot be performed if registrations are still open
self.tirage.enable_do_tirage = True
self.tirage.fermeture = timezone.now() + timedelta(seconds=3600)
self.tirage.save()
resp = self.client.get(self.url)
self.assertTemplateUsed(resp, "tirage-failed.html")
def test_perform_tirage(self):
# Otherwise, perform the tirage
self.tirage.enable_do_tirage = True
self.tirage.fermeture = timezone.now()
self.tirage.save()
resp = self.client.get(self.url)
self.assertTemplateNotUsed(resp, "tirage-failed.html")
class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-liste-spectacles"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/spectacles/{}".format(self.tirage.id)
class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-spectacle"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id, "spectacle_id": self.show1.id}
@property
def url_expected(self):
return "/gestion/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id)
class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-unpaid"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/spectacles/unpaid/{}".format(self.tirage.id)
class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-rappels"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"spectacle_id": self.show1.id}
@property
def url_expected(self):
return "/gestion/bda/mails-rappel/{}".format(self.show1.id)
def test_post(self):
resp = self.client.post(self.url)
self.assertEqual(200, resp.status_code)
# TODO: check that emails are sent
class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
auth_user = None
auth_forbidden = []
bda_testdata = True
def test_api_list(self):
url_list = "/gestion/bda/catalogue/list"
resp = self.client.get(url_list)
# The `list` hook
resp = client.get("/bda/catalogue/list")
self.assertJSONEqual(
resp.content.decode("utf-8"),
[{"id": self.tirage.id, "title": self.tirage.title}],
[{"id": self.tirage.id, "title": self.tirage.title}]
)
def test_api_details(self):
url_details = "/gestion/bda/catalogue/details?id={}".format(self.tirage.id)
resp = self.client.get(url_details)
# The `details` hook
resp = client.get(
"/bda/catalogue/details?id={}".format(self.tirage.id)
)
self.assertJSONEqual(
resp.content.decode("utf-8"),
{
"categories": [{"id": self.category.id, "name": self.category.name}],
"locations": [{"id": self.location.id, "name": self.location.name}],
},
"categories": [{
"id": self.category.id,
"name": self.category.name
}],
"locations": [{
"id": self.location.id,
"name": self.location.name
}],
}
)
def test_api_descriptions(self):
url_descriptions = "/gestion/bda/catalogue/descriptions?id={}".format(
self.tirage.id
# The `descriptions` hook
resp = client.get(
"/bda/catalogue/descriptions?id={}".format(self.tirage.id)
)
resp = self.client.get(url_descriptions)
raw = resp.content.decode("utf-8")
try:
results = json.loads(raw)
@ -322,47 +101,5 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
self.assertEqual(len(results), 3)
self.assertEqual(
{(s["title"], s["price"], s["slots"]) for s in results},
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)},
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)}
)
# ----- BdA Revente --------------------------------------- #
def make_participant(name: str, tirage: Tirage) -> User:
user = User.objects.create_user(username=name, password=name)
user.profile.is_cof = True
user.profile.save()
Participant.objects.create(user=user, tirage=tirage)
return user
class TestReventeManageTest(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="tirage1",
ouverture=timezone.now(),
fermeture=timezone.now() + timedelta(days=90),
)
self.user = make_participant("toto", self.tirage)
self.url = reverse("bda-revente-manage", args=[self.tirage.id])
# Signals handlers on login/logout send messages.
# Due to the way the Django' test Client performs login, this raise an
# error. As workaround, we mock the Django' messages module.
patcher_messages = mock.patch("gestioncof.signals.messages")
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
def test_can_get(self):
client = Client()
client.force_login(
self.user, backend="django.contrib.auth.backends.ModelBackend"
)
r = client.get(self.url)
self.assertEqual(r.status_code, 200)
class TestBdaRevente:
pass
# TODO

View file

@ -1,36 +0,0 @@
from datetime import timedelta
from django.contrib.auth.models import User
from django.utils import timezone
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
def create_user(username, is_cof=False, is_buro=False):
user = User.objects.create_user(username=username, password=username)
user.profile.is_cof = is_cof
user.profile.is_buro = is_buro
user.profile.save()
return user
def user_is_cof(user):
return (user is not None) and user.profile.is_cof
def user_is_staff(user):
return (user is not None) and user.profile.is_buro
def create_spectacle(**kwargs):
defaults = {
"title": "Title",
"category": CategorieSpectacle.objects.first(),
"date": (timezone.now() + timedelta(days=7)).date(),
"location": Salle.objects.first(),
"price": 10.0,
"slots": 20,
"tirage": Tirage.objects.first(),
"listing": False,
}
return Spectacle.objects.create(**dict(defaults, **kwargs))

View file

@ -1,74 +1,61 @@
from django.conf.urls import url
# -*- coding: utf-8 -*-
from bda import views
from bda.views import SpectacleListView
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.conf.urls import url
from gestioncof.decorators import buro_required
from bda.views import SpectacleListView
from bda import views
urlpatterns = [
url(
r"^inscription/(?P<tirage_id>\d+)$",
url(r'^inscription/(?P<tirage_id>\d+)$',
views.inscription,
name="bda-tirage-inscription",
),
url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
url(
r"^spectacles/(?P<tirage_id>\d+)$",
name='bda-tirage-inscription'),
url(r'^places/(?P<tirage_id>\d+)$',
views.places,
name="bda-places-attribuees"),
url(r'^revente/(?P<tirage_id>\d+)$',
views.revente,
name='bda-revente'),
url(r'^etat-places/(?P<tirage_id>\d+)$',
views.etat_places,
name='bda-etat-places'),
url(r'^tirage/(?P<tirage_id>\d+)$', views.tirage),
url(r'^spectacles/(?P<tirage_id>\d+)$',
buro_required(SpectacleListView.as_view()),
name="bda-liste-spectacles",
),
url(
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
name="bda-liste-spectacles"),
url(r'^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$',
views.spectacle,
name="bda-spectacle",
),
url(
r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
views.UnpaidParticipants.as_view(),
name="bda-unpaid",
),
url(
r"^spectacles/autocomplete$",
name="bda-spectacle"),
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
views.unpaid,
name="bda-unpaid"),
url(r'^spectacles/autocomplete$',
views.spectacle_autocomplete,
name="bda-spectacle-autocomplete",
),
url(
r"^participants/autocomplete$",
name="bda-spectacle-autocomplete"),
url(r'^participants/autocomplete$',
views.participant_autocomplete,
name="bda-participant-autocomplete",
),
# Urls BdA-Revente
url(
r"^revente/(?P<tirage_id>\d+)/manage$",
views.revente_manage,
name="bda-revente-manage",
),
url(
r"^revente/(?P<tirage_id>\d+)/subscribe$",
views.revente_subscribe,
name="bda-revente-subscribe",
),
url(
r"^revente/(?P<tirage_id>\d+)/tirages$",
views.revente_tirages,
name="bda-revente-tirages",
),
url(
r"^revente/(?P<spectacle_id>\d+)/buy$",
views.revente_buy,
name="bda-revente-buy",
),
url(
r"^revente/(?P<revente_id>\d+)/confirm$",
views.revente_confirm,
name="bda-revente-confirm",
),
url(
r"^revente/(?P<tirage_id>\d+)/shotgun$",
name="bda-participant-autocomplete"),
url(r'^liste-revente/(?P<tirage_id>\d+)$',
views.list_revente,
name="bda-liste-revente"),
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
views.buy_revente,
name="bda-buy-revente"),
url(r'^revente-interested/(?P<revente_id>\d+)$',
views.revente_interested,
name='bda-revente-interested'),
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
views.revente_shotgun,
name="bda-revente-shotgun",
),
url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"),
name="bda-shotgun"),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$',
views.send_rappel,
name="bda-rappels"
),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'),
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue,
name='bda-catalogue'),
]

File diff suppressed because it is too large Load diff

View file

@ -1 +0,0 @@
default_app_config = "bds.apps.BdsConfig"

View file

@ -1,5 +0,0 @@
from django.contrib import admin
from bds.models import BDSProfile
admin.site.register(BDSProfile)

View file

@ -1,29 +0,0 @@
from django import apps as global_apps
from django.apps import AppConfig
from django.db.models import Q
from django.db.models.signals import post_migrate
def bds_group_perms(app_config, apps=global_apps, **kwargs):
try:
Permission = apps.get_model("auth", "Permission")
Group = apps.get_model("auth", "Group")
group = Group.objects.get(name="Burô du BDS")
perms = Permission.objects.filter(
Q(content_type__app_label="bds")
| Q(content_type__app_label="auth") & Q(content_type__model="user")
)
group.permissions.set(perms)
group.save()
except (LookupError, Group.DoesNotExist):
return
class BdsConfig(AppConfig):
name = "bds"
verbose_name = "Gestion des adhérent·e·s du BDS"
def ready(self):
post_migrate.connect(bds_group_perms, sender=self)

View file

@ -1,63 +0,0 @@
from urllib.parse import urlencode
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from shared import autocomplete
User = get_user_model()
class BDSMemberSearch(autocomplete.ModelSearch):
model = User
search_fields = ["username", "first_name", "last_name"]
verbose_name = _("Membres du BDS")
def get_queryset_filter(self, *args, **kwargs):
qset_filter = super().get_queryset_filter(*args, **kwargs)
qset_filter &= Q(bds__is_member=True)
return qset_filter
def result_uuid(self, user):
return user.username
def result_link(self, user):
return reverse("bds:user.update", args=(user.pk,))
class BDSOthersSearch(autocomplete.ModelSearch):
model = User
search_fields = ["username", "first_name", "last_name"]
verbose_name = _("Non-membres du BDS")
def get_queryset_filter(self, *args, **kwargs):
qset_filter = super().get_queryset_filter(*args, **kwargs)
qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False)
return qset_filter
def result_uuid(self, user):
return user.username
def result_link(self, user):
return reverse("bds:user.update", args=(user.pk,))
class BDSLDAPSearch(autocomplete.LDAPSearch):
def result_link(self, clipper):
url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,))
get = {"fullname": clipper.fullname, "mail": clipper.mail}
return "{}?{}".format(url, urlencode(get))
class BDSSearch(autocomplete.Compose):
search_units = [
("members", BDSMemberSearch()),
("others", BDSOthersSearch()),
("clippers", BDSLDAPSearch()),
]
bds_search = BDSSearch()

View file

@ -1,41 +0,0 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
from bds.models import BDSProfile
User = get_user_model()
class UserForm(forms.ModelForm):
is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False)
class Meta:
model = User
fields = ["email", "first_name", "last_name"]
class UserFromClipperForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].disabled = True
class Meta:
model = User
fields = ["username", "email", "first_name", "last_name"]
class UserFromScratchForm(UserCreationForm):
class Meta:
model = User
fields = ["username", "email", "first_name", "last_name"]
class ProfileForm(forms.ModelForm):
class Meta:
model = BDSProfile
exclude = ["user"]
widgets = {
"birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d")
}

View file

@ -1,141 +0,0 @@
# Generated by Django 2.2 on 2019-07-17 12:48
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import bds.models
class Migration(migrations.Migration):
initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name="BDSProfile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"phone",
models.CharField(
blank=True, max_length=20, verbose_name="téléphone"
),
),
(
"occupation",
models.CharField(
choices=[
("EXT", "Extérieur"),
("1A", "1A"),
("2A", "2A"),
("3A", "3A"),
("4A", "4A"),
("MAG", "Magistérien"),
("ARC", "Archicube"),
("DOC", "Doctorant"),
("CST", "CST"),
("PER", "Personnel ENS"),
],
default="1A",
max_length=3,
verbose_name="occupation",
),
),
(
"departement",
models.CharField(
blank=True, max_length=50, verbose_name="département"
),
),
(
"birthdate",
models.DateField(
blank=True, null=True, verbose_name="date de naissance"
),
),
(
"mails_bds",
models.BooleanField(
default=False, verbose_name="recevoir les mails du BDS"
),
),
(
"is_buro",
models.BooleanField(
default=False, verbose_name="membre du Burô du BDS"
),
),
(
"has_certificate",
models.BooleanField(
default=False, verbose_name="certificat médical"
),
),
(
"certificate_file",
models.FileField(
blank=True,
upload_to=bds.models.BDSProfile.get_certificate_filename,
verbose_name="fichier de certificat médical",
),
),
(
"ASPSL_number",
models.CharField(
blank=True,
max_length=50,
null=True,
verbose_name="numéro AS PSL",
),
),
(
"FFSU_number",
models.CharField(
blank=True, max_length=50, null=True, verbose_name="numéro FFSU"
),
),
(
"cotisation_period",
models.CharField(
choices=[
("ANN", "Année"),
("SE1", "Premier semestre"),
("SE2", "Deuxième semestre"),
("NO", "Aucune"),
],
default="NO",
max_length=3,
verbose_name="inscription",
),
),
(
"registration_date",
models.DateField(
auto_now_add=True, verbose_name="date d'inscription"
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="bds",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Profil BDS",
"verbose_name_plural": "Profils BDS",
},
)
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.2 on 2019-07-17 14:56
from django.db import migrations
def create_bds_buro_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="Burô du BDS")
class Migration(migrations.Migration):
dependencies = [("bds", "0001_initial")]
operations = [
migrations.RunPython(create_bds_buro_group, migrations.RunPython.noop)
]

View file

@ -1,24 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-20 22:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bds", "0002_bds_group"),
]
operations = [
migrations.AlterModelOptions(
name="bdsprofile",
options={
"permissions": (("is_team", "est membre du burô"),),
"verbose_name": "Profil BDS",
"verbose_name_plural": "Profils BDS",
},
),
migrations.RemoveField(
model_name="bdsprofile",
name="is_buro",
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-22 10:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bds", "0003_staff_permission"),
]
operations = [
migrations.AddField(
model_name="bdsprofile",
name="cotisation_type",
field=models.CharField(
choices=[
("ETU", "Étudiant"),
("NOR", "Normalien"),
("EXT", "Extérieur"),
("ARC", "Archicube"),
],
default="Normalien",
max_length=9,
verbose_name="type de cotisation",
),
preserve_default=False,
),
migrations.AddField(
model_name="bdsprofile",
name="is_member",
field=models.BooleanField(default=False, verbose_name="adhérent⋅e du BDS"),
),
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.2.14 on 2020-07-27 20:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bds", "0004_is_member_cotiz_type"),
]
operations = [
migrations.RemoveField(
model_name="bdsprofile",
name="certificate_file",
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 2.2.12 on 2020-08-28 12:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bds", "0005_remove_bdsprofile_certificate_file"),
]
operations = [
migrations.AddField(
model_name="bdsprofile",
name="comments",
field=models.TextField(
blank=True,
help_text="Attention : l'utilisateur·ice dispose d'un droit d'accès"
" aux données le/la concernant, dont le contenu de ce champ !",
verbose_name="commentaires",
),
),
]

View file

@ -1,122 +0,0 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
class StaffRequiredMixin(PermissionRequiredMixin):
permission_required = "bds.is_team"
class MultipleFormMixin(ContextMixin):
"""Mixin pour gérer plusieurs formulaires dans la même vue.
Le fonctionnement est relativement identique à celui de
FormMixin, dont la documentation est disponible ici :
https://docs.djangoproject.com/en/3.0/ref/class-based-views/mixins-editing/
Les principales différences sont :
- au lieu de form_class, il faut donner comme attribut un dict de la forme
{<form_name>: <form_class>}, avec tous les formulaires à instancier. On
peut aussi redéfinir `get_form_classes`
- les données initiales se récupèrent pour chaque form via l'attribut
`<form_name>_initial` ou la fonction `get_<form_name>_initial`. De même,
si certaines forms sont des `ModelForm`s, on peut définir la fonction
`get_<form_name>_instance`.
- chaque form a un préfixe rajouté, par défaut <form_name>, mais qui peut
être customisé via `prefixes` ou `get_prefixes`.
"""
form_classes = {}
prefixes = {}
initial = {}
success_url = None
def get_form_classes(self):
return self.form_classes
def get_initial(self, form_name):
initial_attr = "%s_initial" % form_name
initial_method = "get_%s_initial" % form_name
initial_method = getattr(self, initial_method, None)
if hasattr(self, initial_attr):
return getattr(self, initial_attr)
elif callable(initial_method):
return initial_method()
else:
return self.initial.copy()
def get_prefix(self, form_name):
return self.prefixes.get(form_name, form_name)
def get_instance(self, form_name):
# Au cas où certaines des forms soient des ModelForms
instance_method = "get_%s_instance" % form_name
instance_method = getattr(self, instance_method, None)
if callable(instance_method):
return instance_method()
else:
return None
def get_form_kwargs(self, form_name):
kwargs = {
"initial": self.get_initial(form_name),
"prefix": self.get_prefix(form_name),
"instance": self.get_instance(form_name),
}
if self.request.method in ("POST", "PUT"):
kwargs.update({"data": self.request.POST, "files": self.request.FILES})
return kwargs
def get_forms(self):
form_classes = self.get_form_classes()
return {
form_name: form_class(**self.get_form_kwargs(form_name))
for form_name, form_class in form_classes.items()
}
def get_success_url(self):
if not self.success_url:
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
return str(self.success_url)
def form_valid(self, forms):
# on garde le nom form_valid pour l'interface avec SuccessMessageMixin
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, forms):
"""If the form is invalid, render the invalid form."""
return self.render_to_response(self.get_context_data(forms=forms))
class ProcessMultipleFormView(View):
"""Équivalent de `ProcessFormView` pour plusieurs forms.
Note : il faut que *tous* les formulaires soient valides pour
qu'ils soient sauvegardés !
"""
def get(self, request, *args, **kwargs):
forms = self.get_forms()
return self.render_to_response(self.get_context_data(forms=forms))
def post(self, request, *args, **kwargs):
forms = self.get_forms()
if all(form.is_valid() for form in forms.values()):
return self.form_valid(forms)
else:
return self.form_invalid(forms)
class BaseMultipleFormView(MultipleFormMixin, ProcessMultipleFormView):
pass
class MultipleFormView(TemplateResponseMixin, BaseMultipleFormView):
pass

View file

@ -1,113 +0,0 @@
from datetime import date
from os.path import splitext
from django.contrib.auth import get_user_model
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from shared.utils import choices_length
User = get_user_model()
class BDSProfile(models.Model):
OCCUPATION_CHOICES = (
("EXT", "Extérieur"),
("1A", "1A"),
("2A", "2A"),
("3A", "3A"),
("4A", "4A"),
("MAG", "Magistérien"),
("ARC", "Archicube"),
("DOC", "Doctorant"),
("CST", "CST"),
("PER", "Personnel ENS"),
)
TYPE_COTIZ_CHOICES = (
("ETU", "Étudiant"),
("NOR", "Normalien"),
("EXT", "Extérieur"),
("ARC", "Archicube"),
)
COTIZ_DURATION_CHOICES = (
("ANN", "Année"),
("SE1", "Premier semestre"),
("SE2", "Deuxième semestre"),
("NO", "Aucune"),
)
def get_certificate_filename(instance, filename):
_, ext = splitext(filename) # récupère l'extension du fichier
year = str(date.now().year)
return "certifs/{username}-{year}.{ext}".format(
username=instance.user.username, year=year, ext=ext
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="bds")
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
occupation = models.CharField(
_("occupation"),
default="1A",
choices=OCCUPATION_CHOICES,
max_length=choices_length(OCCUPATION_CHOICES),
)
departement = models.CharField(_("département"), max_length=50, blank=True)
birthdate = models.DateField(
auto_now_add=False,
auto_now=False,
verbose_name=_("date de naissance"),
blank=True,
null=True,
)
is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False)
mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False)
has_certificate = models.BooleanField(_("certificat médical"), default=False)
ASPSL_number = models.CharField(
_("numéro AS PSL"), max_length=50, blank=True, null=True
)
FFSU_number = models.CharField(
_("numéro FFSU"), max_length=50, blank=True, null=True
)
cotisation_period = models.CharField(
_("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3
)
registration_date = models.DateField(
auto_now_add=True, verbose_name=_("date d'inscription")
)
cotisation_type = models.CharField(
_("type de cotisation"), choices=TYPE_COTIZ_CHOICES, max_length=9
)
comments = models.TextField(
_("commentaires"),
blank=True,
help_text=_(
"Attention : l'utilisateur·ice dispose d'un droit d'accès aux données "
"le/la concernant, dont le contenu de ce champ !"
),
)
@classmethod
def expired_members(cls):
now = timezone.now()
qs = cls.objects.filter(is_member=True)
if now.month > 1 and now.month < 7:
return qs.filter(cotisation_period="SE1")
elif now.month < 2 or now.month > 8:
return qs.none()
return qs
class Meta:
verbose_name = _("Profil BDS")
verbose_name_plural = _("Profils BDS")
permissions = (("is_team", _("est membre du burô")),)
def __str__(self):
return self.user.username

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,15 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="300px" height="246px" viewBox="0 0 3000 2460" preserveAspectRatio="xMidYMid meet">
<g id="layer101" fill="#ffffff" stroke="none">
<path d="M0 1230 l0 -1230 1500 0 1500 0 0 1230 0 1230 -1500 0 -1500 0 0 -1230z"/>
</g>
<g id="layer102" fill="#3e2263" stroke="none">
<path d="M0 1230 l0 -1230 1500 0 1500 0 0 1230 0 1230 -1500 0 -1500 0 0 -1230z m1716 765 c103 -17 204 -46 284 -83 l55 -25 -40 -8 c-80 -16 -169 -10 -245 16 -123 42 -232 59 -385 59 -126 0 -278 -18 -341 -40 -13 -5 -16 -4 -9 4 14 15 166 60 254 76 102 19 315 19 427 1z m-703 -217 c15 -6 27 -13 27 -17 0 -4 -37 -25 -82 -47 -86 -43 -182 -120 -193 -155 -8 -26 32 -113 78 -167 44 -52 46 -62 10 -62 -39 1 -106 33 -165 80 -40 31 -54 38 -64 29 -21 -18 -17 -50 16 -116 57 -113 151 -199 259 -235 49 -16 51 -18 16 -13 -123 19 -260 151 -322 310 -15 39 -14 -81 1 -147 25 -107 55 -176 112 -251 49 -65 52 -73 38 -88 -21 -23 -104 -62 -216 -100 -51 -18 -97 -35 -102 -40 -4 -4 32 -10 80 -14 106 -9 256 13 317 48 l39 22 56 -33 c32 -18 114 -61 184 -95 80 -39 126 -67 123 -74 -6 -19 12 -16 19 3 9 24 28 5 22 -22 -7 -26 8 -34 18 -9 11 29 28 16 21 -18 -6 -30 -5 -31 10 -13 8 11 15 24 15 29 0 4 5 5 10 2 6 -3 8 -18 4 -33 -5 -26 -5 -26 10 -8 15 19 18 19 118 -12 113 -35 284 -72 329 -72 24 0 29 4 29 25 0 34 -36 125 -63 158 l-22 28 -59 -30 c-74 -39 -165 -43 -233 -10 -24 11 -43 25 -43 30 0 4 28 6 63 2 49 -4 71 -2 98 12 46 23 79 55 79 75 0 16 -48 66 -135 139 l-40 33 43 -23 c55 -30 166 -121 188 -154 9 -13 13 -29 9 -36 -15 -23 -100 -69 -129 -69 -17 0 -38 -5 -46 -10 -12 -8 -10 -10 10 -10 49 1 104 19 149 51 l45 31 24 -28 c33 -40 72 -128 79 -180 10 -79 -25 -96 -233 -114 -84 -7 -148 -7 -218 0 -106 12 -269 51 -317 76 -28 14 -31 13 -62 -10 -56 -43 -167 -89 -225 -94 l-55 -5 6 29 c2 16 14 55 26 86 21 57 27 112 15 142 -3 9 -34 38 -68 64 l-62 49 -104 7 c-166 12 -192 34 -83 70 34 12 65 24 68 28 4 4 -7 27 -24 52 -118 171 -124 402 -16 571 18 29 50 88 70 131 42 93 99 154 168 184 82 35 212 44 280 18z m540 -39 c60 -16 70 -41 75 -196 l5 -134 -39 3 -39 3 -5 114 c-4 93 -8 116 -22 125 -31 19 -41 -7 -47 -126 l-6 -113 -32 -3 c-41 -4 -44 6 -34 151 9 143 25 172 101 186 3 0 22 -4 43 -10z m322 -34 c0 -30 0 -30 -64 -33 l-64 -3 7 -130 7 -129 -35 0 -36 0 0 165 0 166 93 -3 92 -3 0 -30z m115 -55 l5 -84 18 27 c21 33 39 34 60 5 15 -22 16 -18 16 60 l1 82 35 0 35 0 -2 -162 -3 -163 -40 0 c-38 0 -41 2 -53 43 -16 49 -24 48 -42 -7 -12 -38 -16 -41 -50 -41 -34 0 -38 3 -44 31 -8 43 -8 284 1 293 4 4 19 6 33 4 24 -3 25 -6 30 -88z m370 -42 c193 -243 228 -505 100 -759 -61 -120 -195 -260 -328 -343 -66 -41 -60 -26 13 33 36 29 92 85 125 126 182 230 209 500 75 753 -42 79 -45 89 -45 163 0 44 4 79 9 79 5 0 28 -24 51 -52z m-1111 -294 c76 -30 94 -102 41 -156 -27 -27 -29 -32 -17 -47 34 -40 25 -100 -19 -120 -30 -13 -143 -15 -164 -1 -11 7 -15 42 -18 164 l-4 156 23 9 c37 15 115 13 158 -5z m571 -9 c84 -43 61 -135 -43 -173 -46 -18 -52 -23 -52 -48 0 -39 30 -42 79 -9 l37 25 -3 -41 c-4 -49 -32 -76 -90 -85 -32 -6 -41 -2 -68 24 -24 25 -30 39 -30 74 0 57 21 85 87 114 40 18 54 29 51 42 -5 29 -71 28 -111 -2 -19 -14 -36 -26 -39 -26 -11 0 -16 55 -7 72 24 45 131 63 189 33z m-303 -12 c56 -26 75 -65 71 -150 -3 -67 -6 -75 -38 -108 -43 -44 -94 -61 -146 -48 l-39 9 -3 144 c-1 79 0 150 2 157 8 19 110 17 153 -4z"/>
<path d="M1503 633 c4 -3 10 -3 14 0 3 4 0 7 -7 7 -7 0 -10 -3 -7 -7z"/>
<path d="M1532 448 c3 -7 15 -14 29 -16 23 -2 23 -2 5 13 -24 18 -39 20 -34 3z"/>
<path d="M1140 1231 c0 -45 10 -61 36 -61 26 0 64 32 64 53 0 23 -23 37 -62 37 -35 0 -38 -2 -38 -29z"/>
<path d="M1140 1070 c0 -35 17 -45 56 -36 20 5 25 12 22 34 -3 23 -8 27 -40 30 -36 3 -38 2 -38 -28z"/>
<path d="M1430 1151 c0 -72 3 -91 14 -91 28 0 47 13 61 41 31 60 10 121 -47 135 l-28 6 0 -91z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 2560 2560"
height="256"
width="256"
version="1.0">
<g
transform="translate(-220,50)"
stroke="none"
fill="#ffffff"
id="layer101">
<path
id="path2"
d="M 0,1230 V 0 H 1500 3000 V 1230 2460 H 1500 0 Z" />
</g>
<g
transform="translate(-220,50)"
stroke="none"
fill="#3e2263"
id="layer102">
<path
id="path5"
d="M 220,1230 V -50 H 1500 2780 V 1230 2510 H 1500 220 Z m 1496,765 c 103,-17 204,-46 284,-83 l 55,-25 -40,-8 c -80,-16 -169,-10 -245,16 -123,42 -232,59 -385,59 -126,0 -278,-18 -341,-40 -13,-5 -16,-4 -9,4 14,15 166,60 254,76 102,19 315,19 427,1 z m -703,-217 c 15,-6 27,-13 27,-17 0,-4 -37,-25 -82,-47 -86,-43 -182,-120 -193,-155 -8,-26 32,-113 78,-167 44,-52 46,-62 10,-62 -39,1 -106,33 -165,80 -40,31 -54,38 -64,29 -21,-18 -17,-50 16,-116 57,-113 151,-199 259,-235 49,-16 51,-18 16,-13 -123,19 -260,151 -322,310 -15,39 -14,-81 1,-147 25,-107 55,-176 112,-251 49,-65 52,-73 38,-88 -21,-23 -104,-62 -216,-100 -51,-18 -97,-35 -102,-40 -4,-4 32,-10 80,-14 106,-9 256,13 317,48 l 39,22 56,-33 c 32,-18 114,-61 184,-95 80,-39 126,-67 123,-74 -6,-19 12,-16 19,3 9,24 28,5 22,-22 -7,-26 8,-34 18,-9 11,29 28,16 21,-18 -6,-30 -5,-31 10,-13 8,11 15,24 15,29 0,4 5,5 10,2 6,-3 8,-18 4,-33 -5,-26 -5,-26 10,-8 15,19 18,19 118,-12 113,-35 284,-72 329,-72 24,0 29,4 29,25 0,34 -36,125 -63,158 l -22,28 -59,-30 c -74,-39 -165,-43 -233,-10 -24,11 -43,25 -43,30 0,4 28,6 63,2 49,-4 71,-2 98,12 46,23 79,55 79,75 0,16 -48,66 -135,139 l -40,33 43,-23 c 55,-30 166,-121 188,-154 9,-13 13,-29 9,-36 -15,-23 -100,-69 -129,-69 -17,0 -38,-5 -46,-10 -12,-8 -10,-10 10,-10 49,1 104,19 149,51 l 45,31 24,-28 c 33,-40 72,-128 79,-180 10,-79 -25,-96 -233,-114 -84,-7 -148,-7 -218,0 -106,12 -269,51 -317,76 -28,14 -31,13 -62,-10 -56,-43 -167,-89 -225,-94 l -55,-5 6,29 c 2,16 14,55 26,86 21,57 27,112 15,142 -3,9 -34,38 -68,64 l -62,49 -104,7 c -166,12 -192,34 -83,70 34,12 65,24 68,28 4,4 -7,27 -24,52 -118,171 -124,402 -16,571 18,29 50,88 70,131 42,93 99,154 168,184 82,35 212,44 280,18 z m 540,-39 c 60,-16 70,-41 75,-196 l 5,-134 -39,3 -39,3 -5,114 c -4,93 -8,116 -22,125 -31,19 -41,-7 -47,-126 l -6,-113 -32,-3 c -41,-4 -44,6 -34,151 9,143 25,172 101,186 3,0 22,-4 43,-10 z m 322,-34 c 0,-30 0,-30 -64,-33 l -64,-3 7,-130 7,-129 h -35 -36 v 165 166 l 93,-3 92,-3 z m 115,-55 5,-84 18,27 c 21,33 39,34 60,5 15,-22 16,-18 16,60 l 1,82 h 35 35 l -2,-162 -3,-163 h -40 c -38,0 -41,2 -53,43 -16,49 -24,48 -42,-7 -12,-38 -16,-41 -50,-41 -34,0 -38,3 -44,31 -8,43 -8,284 1,293 4,4 19,6 33,4 24,-3 25,-6 30,-88 z m 370,-42 c 193,-243 228,-505 100,-759 -61,-120 -195,-260 -328,-343 -66,-41 -60,-26 13,33 36,29 92,85 125,126 182,230 209,500 75,753 -42,79 -45,89 -45,163 0,44 4,79 9,79 5,0 28,-24 51,-52 z M 1249,1314 c 76,-30 94,-102 41,-156 -27,-27 -29,-32 -17,-47 34,-40 25,-100 -19,-120 -30,-13 -143,-15 -164,-1 -11,7 -15,42 -18,164 l -4,156 23,9 c 37,15 115,13 158,-5 z m 571,-9 c 84,-43 61,-135 -43,-173 -46,-18 -52,-23 -52,-48 0,-39 30,-42 79,-9 l 37,25 -3,-41 c -4,-49 -32,-76 -90,-85 -32,-6 -41,-2 -68,24 -24,25 -30,39 -30,74 0,57 21,85 87,114 40,18 54,29 51,42 -5,29 -71,28 -111,-2 -19,-14 -36,-26 -39,-26 -11,0 -16,55 -7,72 24,45 131,63 189,33 z m -303,-12 c 56,-26 75,-65 71,-150 -3,-67 -6,-75 -38,-108 -43,-44 -94,-61 -146,-48 l -39,9 -3,144 c -1,79 0,150 2,157 8,19 110,17 153,-4 z" />
<path
id="path7"
d="m 1503,633 c 4,-3 10,-3 14,0 3,4 0,7 -7,7 -7,0 -10,-3 -7,-7 z" />
<path
id="path9"
d="m 1532,448 c 3,-7 15,-14 29,-16 23,-2 23,-2 5,13 -24,18 -39,20 -34,3 z" />
<path
id="path11"
d="m 1140,1231 c 0,-45 10,-61 36,-61 26,0 64,32 64,53 0,23 -23,37 -62,37 -35,0 -38,-2 -38,-29 z" />
<path
id="path13"
d="m 1140,1070 c 0,-35 17,-45 56,-36 20,5 25,12 22,34 -3,23 -8,27 -40,30 -36,3 -38,2 -38,-28 z" />
<path
id="path15"
d="m 1430,1151 c 0,-72 3,-91 14,-91 28,0 47,13 61,41 31,60 10,121 -47,135 l -28,6 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

Some files were not shown because too many files have changed in this diff Show more