Compare commits

...

267 Commits

Author SHA1 Message Date
Antoine Bartuccio 3501703c15 eboutic: don't display future account balance if contains refilling item 2019-11-05 19:50:08 +01:00
Antoine Bartuccio 129f2e53ee Merge branch 'galaRequests' into 'master'
eticketListView: product id instead of eticket id

See merge request ae/Sith!257
2019-11-05 11:23:52 +01:00
Antoine Bartuccio 209867b3a8
black: makes new version happy 2019-11-04 13:46:09 +01:00
Robin Trioux 59511d255f eticketListView: product id instead of eticket id 2019-11-03 20:58:20 +01:00
Antoine Bartuccio f42daa01c5 Merge branch 'add-account-amount' into 'master'
Add the account amount to the eboutic

See merge request ae/Sith!254
2019-10-28 23:37:33 +01:00
tleb 29ee1b05af Merge branch 'master' into 'add-account-amount'
# Conflicts:
#   locale/fr/LC_MESSAGES/django.po
2019-10-28 15:48:54 +01:00
Antoine Bartuccio 42055b9001 Merge branch 'auto-uv-pedagogy' into 'master'
Auto fill UVs in pedagogy

See merge request ae/Sith!253
2019-10-25 12:09:39 +02:00
tleb 00c96f5b71 eboutic: fix account amount 2019-10-24 14:40:26 +02:00
tleb 5cc7eff94f pedagogy: uv autofill finishing touches 2019-10-24 14:18:29 +02:00
Antoine Bartuccio 28077ef0b0 Merge branch 'fix-create-club' into 'master'
club: fix 500 on club_new

See merge request ae/Sith!256
2019-10-22 08:34:08 +02:00
tleb 143b128891 club: fix 500 on club_new 2019-10-21 22:56:24 +02:00
tleb 6b06b647bc eboutic: add p tag in makecommand 2019-10-21 22:10:38 +02:00
tleb 413c613c9f Fix translation for basket account amount 2019-10-21 22:06:56 +02:00
tleb 1c0d15ba2a settings: fix black report 2019-10-21 21:17:11 +02:00
tleb 28bd6b8708 uv: make autofill available on edit page 2019-10-21 17:10:16 +02:00
tleb 419a48ac3a /pedagogy/uv/create put urls in settings 2019-10-21 17:01:21 +02:00
tleb 6fce27113a /pedagogy/uv/create use quick notif 2019-10-21 16:52:51 +02:00
tleb 53a7633700 uv: Add error handler to uv autofill 2019-10-21 10:34:46 +02:00
tleb 4094394cef api: typo in doc comment 2019-10-21 08:13:36 +02:00
Antoine Bartuccio f533c39e67
api: fix uv manager acquisition if uv is only available in spring 2019-10-21 02:10:09 +02:00
tleb 86bc491df4 Fix UV_endpoint auth 2019-10-20 18:26:11 +02:00
tleb 4759551c16 Autofull UV small changes 2019-10-20 17:09:36 +02:00
tleb b057dbfd60 Initial add account amount in eboutic 2019-10-18 21:41:39 +02:00
tleb bddb88d97f Comment UV API and fix little bugs 2019-10-18 18:13:53 +02:00
tleb dbe44a9c1c Fix hour count and submit button 2019-10-18 01:45:49 +02:00
tleb eeb791c460 Initial autofill on UV 2019-10-18 01:28:59 +02:00
Antoine Bartuccio 6d0eba6bcf Merge branch 'rework-front' into 'master'
Markdown widget follows the required attribute

See merge request ae/Sith!249
2019-10-17 14:47:56 +02:00
Antoine Bartuccio 4d04b21f04 Merge branch 'cleanup-forms' into 'master'
Remove unused multiple-select library

See merge request ae/Sith!250
2019-10-17 14:46:02 +02:00
Antoine Bartuccio 2f1b26053b Merge branch 'fix-news-form' into 'master'
Fix 500 when a news needs a start_date and/or end_date but we don't provide

See merge request ae/Sith!252
2019-10-17 14:42:41 +02:00
Antoine Bartuccio 1848945d64 Merge branch 'bugfix' into 'master'
Fix huge permission problem inducing server memory leaks for etickets

See merge request ae/Sith!251
2019-10-17 12:36:57 +02:00
Antoine Bartuccio 9278419345
core: rename GenericContentPermission into GenericContentPermissionMixinBuilder 2019-10-17 11:56:02 +02:00
Antoine Bartuccio 566dcc7aee
counter: fix Selling view permission 2019-10-17 11:24:52 +02:00
Antoine Bartuccio a6088c0e4a
core: refactor permissions mixins 2019-10-17 11:24:51 +02:00
tleb 60c9498a56 Fix 500 on news creation/edition 2019-10-17 10:25:29 +02:00
Antoine Bartuccio 241650c171
counter: fix eticket server crash induced by old permission system and fix Selling permission 2019-10-16 21:21:51 +02:00
Antoine Bartuccio 811809895e
club: fix mailing list form that unexpectedly relied on try catch in permissions 2019-10-16 21:21:06 +02:00
Antoine Bartuccio fe9164bfef
core: don't use try/except to catch type of view in permissions mixins 2019-10-16 19:28:32 +02:00
tleb ad3f003fbb Remove unused multiple-select library 2019-10-16 14:28:53 +02:00
tleb 7ecb057b68 Isolate easymde instances so that they can be referenced 2019-10-16 12:18:23 +02:00
tleb e932abfa74 Prevent pressing submit if the Markdown widget is empty 2019-10-15 10:41:10 +02:00
tleb 0011f4c7b0 Only register onchange once the submit button has been pressed 2019-10-15 10:23:15 +02:00
tleb 13312e9879 Highlight a markdown input in red if required and submit is pressed
Kind of copy the behaviour of a Firefox input
Once the submit button has been pressed, highlight in red the text
input if it's required but empty
2019-10-15 09:54:10 +02:00
tleb ced90c23db More JS-like, callback as last argument 2019-10-15 09:53:44 +02:00
Antoine Bartuccio 42f5773f51 Merge branch 'fix-guy-feature' into 'master'
Fix the guyguy "feature" on the profile page

See merge request ae/Sith!248
2019-10-15 00:26:04 +02:00
Antoine Bartuccio b270c76249 Merge branch 'galaRequests' into 'master'
Some gala requests: new minor features

See merge request ae/Sith!247
2019-10-14 22:55:55 +02:00
tleb 34df825718 Fix the guyguy "feature" on the profile page 2019-10-14 23:49:32 +03:00
Cyl aac4e3b99c Minor fix for requestGala 2019-10-14 00:32:11 +02:00
Robin Trioux 5a55a6c642 E-ticket link is sent in the email 2019-10-13 19:02:25 +02:00
Robin Trioux 65c3483c1f core:login allow the user to create an account when not logged 2019-10-13 18:43:30 +02:00
Antoine Bartuccio 660a3161f5 Merge branch 'new_django' into 'master'
upgrade to django 2.2

See merge request ae/Sith!243
2019-10-12 23:19:26 +02:00
Antoine Bartuccio 9e6c4b32e3 Merge branch 'bugfix' into 'master'
Fix error when editing poster while being admin

See merge request ae/Sith!245
2019-10-09 18:11:05 +02:00
Antoine Bartuccio 25225fc451
com: fix error when editing poster while being admin 2019-10-09 17:43:23 +02:00
Antoine Bartuccio c3f2d0a134
django2.2: unlock djangorestframework version limit 2019-10-08 22:46:38 +02:00
Antoine Bartuccio cd2d3ee6b4
django2.2: fix tests for accounting 2019-10-08 22:46:38 +02:00
Antoine Bartuccio 81fcf411c1
django2.2: forms fixs for new API 2019-10-08 22:46:38 +02:00
Antoine Bartuccio d7075eb762
django2.2: fix breaking change for getting uploaded files size 2019-10-08 22:46:38 +02:00
Antoine Bartuccio cf3f5ea60c
django2.2: fix django server crash because of breaking change in widget rendering method 2019-10-08 22:46:37 +02:00
Antoine Bartuccio 59185ab2a8
django2.2: rewrite login and password stack because of removed API 2019-10-08 22:46:37 +02:00
Antoine Bartuccio a177fa8232
django2.2: replace deprecated base_name with basename in urls 2019-10-08 22:46:37 +02:00
Antoine Bartuccio 308cf30a5a
django2.2: replace deprecated login view 2019-10-08 22:46:37 +02:00
Antoine Bartuccio 99c8d95443
django2.2: remove direct assignments to many-to-many fields 2019-10-08 22:46:37 +02:00
Antoine Bartuccio 97c316b62e
django2.2: replace removed is_anonymous() and is_authenticated to their now used counterparts 2019-10-08 22:46:03 +02:00
Antoine Bartuccio 90921fd4cd
django2.2: some migrations to make django happy 2019-10-08 22:46:03 +02:00
Antoine Bartuccio 296cc4144c
django2.2: remove SessionAuthenticationMiddleware
See https://docs.djangoproject.com/en/2.2/releases/2.0/ for more details
2019-10-08 22:46:03 +02:00
Antoine Bartuccio f7548ab8d1
django2.2: add on_delete on migrations for OneToOneField 2019-10-08 22:46:03 +02:00
Antoine Bartuccio 3cb306bc91
django2.2: add on_delete on migrations for ForeignKey 2019-10-08 22:46:03 +02:00
Antoine Bartuccio c20d5855e4
django2.2: remove view_page permission as it clash with built-in permission
Need a bit of discussion as I'm not an expert in django built-in permissions
An issue on github says it's related to https://docs.djangoproject.com/en/2.1/releases/2.1/#considerations-for-the-new-model-view-permission
2019-10-08 22:46:02 +02:00
Antoine Bartuccio 00bd60ef4f
django2.2: add exception parameter in forbidden and not_found views 2019-10-08 22:46:02 +02:00
Antoine Bartuccio b8c7fb6f74
django2.2: adapt ChoiceField to the new API 2019-10-08 22:46:02 +02:00
Antoine Bartuccio df531198c9
django2.2: fix urls and adapt them to new format 2019-10-08 22:46:02 +02:00
Antoine Bartuccio 12b6f0d488
django2.2: adapt DateTimeField in forms with the new API 2019-10-08 22:46:02 +02:00
Antoine Bartuccio 6cc234e8d3
django2.2: add default mandatory on_delete on OneToOneField 2019-10-08 22:46:02 +02:00
Antoine Bartuccio 4dadb1dbc0
django2.2: add default mandatory on_delete on all ForeignKey 2019-10-08 22:46:01 +02:00
Antoine Bartuccio 2616e8b24c
django2.2: relpace django.core.urlresolvers by django.urls 2019-10-08 22:46:01 +02:00
Antoine Bartuccio be855c6c90
django2.2: migrate url to re_path 2019-10-08 22:46:01 +02:00
Antoine Bartuccio 7be9077fce Merge branch 'documentation' into 'master'
write a new shiny and comprehensive documentation

See merge request ae/Sith!224
2019-10-08 22:42:19 +02:00
Antoine Bartuccio d48c09a914
documentation: revise title levels on git documentation 2019-10-08 22:13:29 +02:00
Pierre Brunet d5c3dbf864 Add git introduction 2019-10-08 20:22:36 +02:00
Antoine Bartuccio 2a9b89fd2a
core: remove README wiki page that wasn't helpful and anyway broken with rst files 2019-10-08 11:12:22 +02:00
Antoine Bartuccio c73f4ca847
documentation: add CONTRIBUTING 2019-10-08 11:08:47 +02:00
Antoine Bartuccio d63b5335d4
documentation: apply corrections suggested in comments 2019-10-08 11:08:33 +02:00
Antoine Bartuccio a766f7137c
documentation: add instructions for direnv 2019-10-08 00:59:19 +02:00
Antoine Bartuccio 5c3c14ab37
core: wrap compilemessages to avoid compiling whole env and avoid crash at sphinx messages compilation 2019-10-08 00:59:00 +02:00
Antoine Bartuccio 775413ac7e
documentation: weekmail documentation 2019-10-07 23:58:04 +02:00
Antoine Bartuccio 1f271c75f0
documentation: add instructions to add and edit subscriptions 2019-10-07 23:51:38 +02:00
Antoine Bartuccio 4df152185e
documentation: rewrite README and remove TODO 2019-10-07 23:51:38 +02:00
Antoine Bartuccio c83b30f27b
documentation: add doc compile test in pipeline 2019-10-07 23:51:38 +02:00
Antoine Bartuccio db10f7b963
documentation: tutorial about rights management 2019-10-07 23:51:37 +02:00
Antoine Bartuccio ed68c2cb38
documentation: move documentation about usage in production 2019-10-07 23:51:37 +02:00
Antoine Bartuccio a6c8dea190
documentation: move markdown syntax documentation and move helper for counting lines 2019-10-07 23:51:37 +02:00
Antoine Bartuccio 124eaf42cd
documentation: add models manipulation in hello world tutorial 2019-10-07 23:51:37 +02:00
Antoine Bartuccio 5489096bf5
documentation: add explanation on MVT and explain project structure 2019-10-07 23:51:37 +02:00
Antoine Bartuccio 3a425c6792
documentation: add infos about fontawesome + tests instructions + more external doc 2019-10-07 23:51:37 +02:00
Antoine Bartuccio 8809753108
documentation fix tests because of missing README.md 2019-10-07 23:51:37 +02:00
Antoine Bartuccio 4428a2e89c
documentation: add hello world app tutorial 2019-10-07 23:51:36 +02:00
Antoine Bartuccio 0616597bf2
documentation: remove CONTRIBUTING and migrate its content in the doc 2019-10-07 23:51:36 +02:00
Antoine Bartuccio 782a763046
documentation: update instructions, add down loadable files and add all pdf reports for download purpose 2019-10-07 23:51:36 +02:00
Antoine Bartuccio 8dcade6890
documentation: remove Doxygen, include README into doc update tech and install 2019-10-07 23:51:36 +02:00
Antoine Bartuccio dd49d71cb7
documentation: installation instructions 2019-10-07 23:51:36 +02:00
Antoine Bartuccio b0b52fd714
documentation: technologies presentation 2019-10-07 23:51:36 +02:00
Antoine Bartuccio ef40baaa84
documentation: bump sphinx version 2019-10-07 23:51:35 +02:00
Antoine Bartuccio 7c259bf26b
documentation: write introduction 2019-10-07 23:51:35 +02:00
Antoine Bartuccio 05e5008305
documentation: base structure for new documentation 2019-10-07 23:51:35 +02:00
Antoine Bartuccio 448f5ff40f Merge branch 'com' into 'master'
com: fix error 500 when editing poster with com admin

See merge request ae/Sith!242
2019-10-06 17:55:30 +02:00
Antoine Bartuccio 5482f1174d Merge branch 'trombi' into 'master'
trombi: permissions fixs

See merge request ae/Sith!241
2019-10-02 17:30:46 +02:00
Antoine Bartuccio 2da0560ec8
com: fix error 500 when editing poster with com admin 2019-10-02 15:32:33 +02:00
Antoine Bartuccio 5151fc3792
trombi: permissions fixs 2019-10-02 14:56:04 +02:00
Antoine Bartuccio fd5cd56f81 Merge branch 'trombi' into 'master'
Trombi fixs

See merge request ae/Sith!240
2019-09-30 00:00:38 +02:00
Antoine Bartuccio 35d9c05abf
trombi: fix trombi tools if user has a trombi profile but no trombi linked 2019-09-29 12:19:22 +02:00
Antoine Bartuccio fcb3035b67
trombi: fix some 500 errors when accessing page without being in a trombi 2019-09-29 12:09:03 +02:00
Antoine Bartuccio b7db969f08 Merge branch 'counter' into 'master'
[Counter] - Buying group is now required

See merge request ae/Sith!239
2019-09-20 14:03:07 +02:00
Cyl 14303fd46c [Counter] - Buying group is now required 2019-09-19 23:04:03 +02:00
Antoine Bartuccio 5e6b17cd19 Merge branch 'eticket' into 'master'
[Counter] - add the product ID for every Eticket

See merge request ae/Sith!237
2019-09-18 14:42:16 +02:00
Cyl 8232ff59a0 [Counter] - add the product ID for every Eticket 2019-09-16 23:41:06 +02:00
Antoine Bartuccio 411c117f0f Merge branch 'performances' into 'master'
Improve performances on forum and SAS

See merge request ae/Sith!235
2019-09-16 14:23:52 +02:00
Antoine Bartuccio 298499c749
sas: add cache on Picture permissions to avoid hitting the bdd too much 2019-09-16 11:33:32 +02:00
Antoine Bartuccio b8ad2d4835
sas: add pagination on AlbumView 2019-09-16 01:26:20 +02:00
Antoine Bartuccio d37eb134e2 Merge branch 'bugfix' into 'master'
Fix error 500 when editing properties of user without linked customer

See merge request ae/Sith!236
2019-09-15 17:48:21 +02:00
Antoine Bartuccio 63ec5d68f4
core: fix error 500 when editing properties of user without linked customer 2019-09-15 17:05:07 +02:00
Antoine Bartuccio 8330e1eaf2
sas: simplify and optimize permissions for SAS images 2019-09-15 16:43:17 +02:00
Antoine Bartuccio 1f86827e46
core: improve performances on not found images 2019-09-15 16:22:13 +02:00
Antoine Bartuccio 321e5e3ff5
forum: enhance performances on toggle_favorite 2019-09-15 16:12:24 +02:00
Antoine Bartuccio 3eb8292d15 Merge branch 'bugfix' into 'master'
Fix error 500 with expired reset password links

See merge request ae/Sith!234
2019-09-11 10:33:39 +02:00
Antoine Bartuccio 5a3f90fd28
core: fix error 500 with expired reset password links 2019-09-10 16:56:58 +02:00
Antoine Bartuccio eb975f4de1 Merge branch 'performances' into 'master'
Speed up counter click interface and auto scroll

See merge request ae/Sith!233
2019-09-10 15:50:38 +02:00
Antoine Bartuccio 405b938e08
counter: speed counter click interface and auto scroll 2019-09-10 14:22:13 +02:00
Antoine Bartuccio f899e32fb0 Merge branch 'performances' into 'master'
Improve overall performances on notifications, news pages and navbar

See merge request ae/Sith!232
2019-09-09 11:07:04 +02:00
Antoine Bartuccio 9181e77d55
core: add some cache on birthdays and counters_activity 2019-09-09 01:20:15 +02:00
Antoine Bartuccio f1b3a174b6
core: improve performances on notification reads and display 2019-09-09 00:45:08 +02:00
Antoine Bartuccio eb9821ed36 Merge branch 'sas' into 'master'
core: add index on folder and sas properties of SithFile to speed up SAS

See merge request ae/Sith!231
2019-09-06 16:47:18 +02:00
Antoine Bartuccio defb7fb3a3 Merge branch 'pedagogy_v2' into 'master'
Disable having two comments from the same user for the same uv in uv guide

See merge request ae/Sith!229
2019-09-06 16:28:28 +02:00
Antoine Bartuccio 83e225a744
core: add index on folder and sas properties of SithFile to speed up SAS 2019-09-06 16:16:03 +02:00
Antoine Bartuccio f30bea3dc9
pedagogy: add script to remove all previous doubled comments 2019-09-04 20:49:18 +02:00
Antoine Bartuccio a69f7b12b1
pedagogy: add script to remove all previous doubled comments 2019-09-04 20:49:17 +02:00
Antoine Bartuccio ca042fe75e Merge branch 'a19_subscriptions' into 'master'
SUBSCRIPTIONS: 5 new discounted subscriptions for integration

See merge request ae/Sith!226
2019-08-29 19:33:07 +02:00
Zar dc9111dbcd
SUBSCRIPTIONS: 5 new discounted subscriptions for integration 2019-08-29 19:06:58 +02:00
Antoine Bartuccio 8a16a66299 Merge branch 'bugfix' into 'master'
com/core: remove links to index edition that caused error 500

See merge request ae/Sith!230
2019-08-29 18:03:34 +02:00
Antoine Bartuccio d7a7613807
com: add basics tests for ComAlert and ComInfo to detect regressions on tabs 2019-08-29 17:29:38 +02:00
Antoine Bartuccio 3fc8688941
com/core: remove links to index edition that caused error 500 2019-08-29 17:23:27 +02:00
Antoine Bartuccio d7b351a1aa Merge branch 'cyl' into 'master'
[COM] Make the news visible for non-authenticated user and birthday visible for subriber only

See merge request ae/Sith!225
2019-08-29 15:07:08 +02:00
Cyl 9e0c4e70d4 [COM] Make the news visible for non-authenticated user and birthday visible for subriber only 2019-08-28 20:40:31 +02:00
Antoine Bartuccio 2232c495be Merge branch 'bugfix' into 'master'
pedagogy: fix a bug when updating an UV from a different author

See merge request ae/Sith!228
2019-08-28 16:08:32 +02:00
Antoine Bartuccio 18b1bea664 Merge branch 'weekmail' into 'master'
com: add weekmail banner and footer for A19

See merge request ae/Sith!227
2019-08-28 02:35:04 +02:00
Antoine Bartuccio a5d5c41dd6
pedagogy: fix a bug when updating an UV from a different author 2019-08-27 22:46:41 +02:00
Antoine Bartuccio 66d5c71a92
com: add weekmail banner and footer for A19 2019-08-27 18:56:43 +02:00
Antoine Bartuccio 824ea37f44 Merge branch 'pedagogy_v2' into 'master'
Better display on mobile for guide page

See merge request ae/Sith!221
2019-08-26 12:11:26 +02:00
Antoine Bartuccio eb29f98c37 Merge branch 'bugfix' into 'master'
pedagogy: correctly fill star widget when editing comment

Closes #88

See merge request ae/Sith!223
2019-08-20 22:57:02 +02:00
Antoine Bartuccio d903dc58cf
pedagogy: correctly fill star widget when editing comment 2019-08-08 18:59:44 +02:00
Antoine Bartuccio f09de0ab7d
pedagogy: remove stars on small devices for grade 2019-08-08 12:46:51 +02:00
Antoine Bartuccio d29603c584
pedagogy: fix display of guide on smaller devices 2019-08-07 20:03:21 +02:00
Antoine Bartuccio 3380980c5c
pedagogy: add generic font for .radio-guide 2019-08-07 17:50:50 +02:00
Antoine Bartuccio 38ef13d9b6 Merge branch 'markdown-editor' into 'master'
core: upgrade easymde

See merge request ae/Sith!222
2019-07-29 15:28:35 +02:00
Antoine Bartuccio 6c43b1c43d
pedagogy: better display on mobile for guide page
Widen search bar and use a grid template
Reduce zooming when clicking on the search bar (firexfox)
Remove zooming when clicking on search bar (chrome)
2019-07-25 19:01:53 +02:00
Antoine Bartuccio 2b34c46412
core: upgrade easymde 2019-07-19 23:36:56 +02:00
Antoine Bartuccio f9227fa29d Merge branch 'bugfix' into 'master'
Fix error 500 when accessing user tools with anonymous user and fix dependancies

See merge request ae/Sith!220
2019-07-15 15:03:27 +02:00
Antoine Bartuccio 96a3eaff1c
ci: fix django rest framework version 2019-07-15 14:40:03 +02:00
Antoine Bartuccio 65cb85a887
ci: fix building of pygraphviz 2019-07-15 14:29:47 +02:00
Antoine Bartuccio 640a72c52d
core: add tests for UserToolsView 2019-07-15 12:36:05 +02:00
Antoine Bartuccio 9b7b96a310 core: add UserIsLoggedMixin to check if an user is not anonymous 2019-07-15 12:27:19 +02:00
Antoine Bartuccio b18746e769
core: fix error 500 when accessing user tools with anonymous user 2019-07-13 04:58:23 +02:00
Antoine Bartuccio a2b431b1ab Merge branch 'pedagogy_v2' into 'master'
New version of the pedagogy

See merge request ae/Sith!212
2019-07-11 00:05:27 +02:00
Antoine Bartuccio d844bccb04
pedagogy: improve performances on json mode for UVListView 2019-07-10 12:26:37 +02:00
Antoine Bartuccio 49f928e754 Apply suggestion to pedagogy/views.py 2019-07-10 12:12:56 +02:00
Antoine Bartuccio 07fc1014be
pedagogy: put methods after properties in models 2019-07-09 16:59:59 +02:00
Antoine Bartuccio facb6faf75 Merge branch 'pedagogy_v2_front' into 'pedagogy_v2'
Frontend for pedagogy

See merge request ae/Sith!218
2019-07-09 16:57:00 +02:00
Antoine Bartuccio e72338a7d9
pedagogy: enlarge shape around author 2019-07-09 16:33:06 +02:00
Antoine Bartuccio f37c022538
pedagogy: put report button at left and author at right 2019-07-09 16:03:47 +02:00
Antoine Bartuccio 5229628d48
pedagogy: fix weird blank spacing in comment block 2019-07-09 15:08:48 +02:00
Antoine Bartuccio b4b7bf05b4
pedagogy: remove scrolling on desktop for comments 2019-07-09 14:43:46 +02:00
Antoine Bartuccio 231415a772
pedagogy: hide scroll bar at bottom of comments 2019-07-08 23:43:27 +02:00
Antoine Bartuccio f052d307d7
pedagogy: make report button less visible and author more important 2019-07-08 23:16:53 +02:00
Antoine Bartuccio f15971cecf
pedagogy: simplify moderation form for user 2019-07-08 17:34:23 +02:00
Antoine Bartuccio 99cf59c7a4
pedagogy: remove fira font to reduce downloaded content for user 2019-07-08 15:51:31 +02:00
Antoine Bartuccio 0d13014e8a
pedagogy: simpler generation for department radio buttons 2019-07-08 15:36:30 +02:00
Antoine Bartuccio fd1f89de1d
pedagogy: wrap all css inside a class named pedagogy to avoid name clashes 2019-07-08 15:25:28 +02:00
Antoine Bartuccio 78b616427f Merge branch 'new_django' into 'master'
core: rename MIDDLEWARE_CLASSES into MIDDLEWARE

See merge request ae/Sith!219
2019-07-08 15:18:21 +02:00
Antoine Bartuccio c15ea345dd
pedagogy: generate search form radios trough loop and remove semester_translated 2019-07-08 15:17:12 +02:00
Antoine Bartuccio 1d319e90f0
pedagogy: don't make the anchor scroll with comment text 2019-07-08 09:47:54 +02:00
Antoine Bartuccio e6e500e2f9
pedagogy: fix margins on stars for mobile devices 2019-07-08 09:37:30 +02:00
Antoine Bartuccio cf1ec1dc86
pedagogy: add missing markdown treatment for key_concept 2019-07-08 09:26:22 +02:00
Antoine Bartuccio 46a042cde2
pedagogy: fix mobile display on chrome 2019-07-08 09:19:23 +02:00
Antoine Bartuccio 52129d7511
pedagogy: new mobile view and use of css grids for comment display 2019-07-08 02:54:49 +02:00
Antoine Bartuccio d03835d737
pedagogy: allow search on uv title 2019-07-08 00:54:53 +02:00
Antoine Bartuccio b4b7817baa
pedagogy: auto send form when typing 2019-07-08 00:53:02 +02:00
Antoine Bartuccio d85152e58c
pedagogy: quick access to comment from moderation 2019-07-08 00:01:54 +02:00
Antoine Bartuccio f118040432
pedagogy: add pedagogy in user tools 2019-07-08 00:01:38 +02:00
Antoine Bartuccio 9f1aff8c07
pedagogy: add retries on search form and make uv table clickable 2019-07-07 23:52:54 +02:00
Antoine Bartuccio 94bbdf372b
pedagogy: fix css grids on chrome android 2019-07-07 22:14:46 +02:00
Antoine Bartuccio 240d94bd57
pedagogy: enhance display on mobile and fix some bugs with webkit 2019-07-07 21:56:59 +02:00
Antoine Bartuccio 3ee7ff2752
pedagogy: display hours of UVs 2019-07-07 21:38:00 +02:00
Antoine Bartuccio 2c5385cf5c
pedagogy: enhance mobile view 2019-07-07 21:03:27 +02:00
Antoine Bartuccio c8a691044f
pedagogy: add translations 2019-07-07 19:36:47 +02:00
Antoine Bartuccio f93eaff876
pedagogy: small fix for leave comment on desktop 2019-07-07 19:08:30 +02:00
Antoine Bartuccio 10faa14bef
pedagogy: better display on mobile 2019-07-07 19:07:19 +02:00
Antoine Bartuccio 30ccbdc32d
pedagogy: fix search api when searching one letter (case sensitivity) 2019-07-07 18:55:23 +02:00
Antoine Bartuccio 79243aece3
pedagogy: better display for uv_details 2019-07-07 18:51:36 +02:00
Antoine Bartuccio a61322b83f
pedagogy: fix search form display rights 2019-07-07 18:43:43 +02:00
Antoine Bartuccio 3df73f4d1f
pedagogy: css class name consistency 2019-07-07 18:33:56 +02:00
Antoine Bartuccio 7165a63e97
pedagogy: polish uv_details 2019-07-07 18:29:31 +02:00
Antoine Bartuccio 2404edd289
pedagogy: clean up guide page 2019-07-07 16:52:28 +02:00
Antoine Bartuccio 3bff09b04c
pedagogy: correctly display uv infos 2019-07-07 16:33:03 +02:00
Antoine Bartuccio 28748af5d3
pedagogy: smart back button in uv_detail 2019-07-07 14:44:25 +02:00
Antoine Bartuccio a56a4e2cb8
pedagogy: better comment display on mobile 2019-07-06 03:54:46 +02:00
Antoine Bartuccio 339497b2c2
pedagogy: display semester in search view 2019-07-06 02:57:20 +02:00
Antoine Bartuccio c05168a2b5
pedagogy: display most recent comment first 2019-07-06 02:33:05 +02:00
Antoine Bartuccio 782ee35779
pedagogy: incorpore all elements in comment block 2019-07-06 02:30:47 +02:00
Antoine Bartuccio 43acee8f1b
pedagogy: enhance comment look 2019-07-06 02:16:04 +02:00
Antoine Bartuccio 4a19441a17
pedagogy: translations for semesters in details and handle markdown 2019-07-06 01:16:09 +02:00
Antoine Bartuccio 11acf5897f
pedagogy: correctly hide AP input with its label 2019-07-06 00:34:41 +02:00
root 4be99fe828 guide design not finished 2019-07-06 00:14:15 -05:00
root 601193ff3c small change comment 2019-07-05 22:42:13 -05:00
Antoine Bartuccio f500dec1f1 core: rename MIDDLEWARE_CLASSES into MIDDLEWARE
MIDDLEWARE_CLASSES is deprecated since django 1.11 and with last version of django-debug-toolbar it broke the dev server
See here for more details https://docs.djangoproject.com/fr/2.2/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware
2019-07-05 22:01:30 +02:00
Cyl bfb7380715
[Pedagogy] Front comment nearly ended 2019-07-05 20:54:14 +02:00
Antoine Bartuccio 3e3c576ad7
pedagogy: live uv update on guide 2019-07-05 20:11:33 +02:00
Antoine Bartuccio 2aa1314fac
pedagogy: basic display of the guide 2019-07-05 18:16:28 +02:00
Antoine Bartuccio 3063e4a24f
pedagogy: auto fill search form from get arguments in URL 2019-07-05 16:40:14 +02:00
Antoine Bartuccio 6f8ec4740c
pedagogy: simpler user interface for autumn and spring search 2019-07-05 16:40:14 +02:00
Antoine Bartuccio cbcd84c931
pedagogy: add a search form 2019-07-05 16:40:14 +02:00
Antoine Bartuccio e475273cd3
pedagogy: enhance StarList widget 2019-07-05 16:40:14 +02:00
Cyl 851231869b
[pedagogy] Star for comment grades 2019-07-05 16:40:13 +02:00
Antoine Bartuccio 3376f4dfb4
pedagogy: fix typo for AUTUMN_AND_SPRING 2019-07-05 16:40:02 +02:00
Antoine Bartuccio 205f93569a
pedagogy: grade averages for UVs 2019-07-04 18:07:51 +02:00
Antoine Bartuccio 4f7a8661ba Merge branch 'pedagogy_v2_moderation' into 'pedagogy_v2'
Pedagogy comments moderation

See merge request ae/Sith!215
2019-07-04 15:54:24 +02:00
Antoine Bartuccio 6e7d351e8e
pedagogy: send notification to pedagogy admins at comment report 2019-07-04 15:32:00 +02:00
Antoine Bartuccio 73f1927ce4 Merge branch 'pedagogy_v2_old_base_import' into 'pedagogy_v2'
pedagogy: fix bdd id out of sync after old data base import

See merge request ae/Sith!216
2019-06-25 15:24:46 +02:00
Antoine Bartuccio 56e3f39de1 pedagogy: fix bdd id out of sync after old data base import 2019-06-20 19:20:06 +02:00
Antoine Bartuccio 75a2aefd69
pedagogy: display if comment is reported 2019-06-20 15:03:51 +02:00
Antoine Bartuccio 55e822412a
pedagogy: full test suite for pedagogy moderation 2019-06-20 14:57:58 +02:00
Antoine Bartuccio 171d9a4381
pedagogy: tests and fixs for uv moderation form 2019-06-20 14:22:06 +02:00
Antoine Bartuccio 806084e707
pedagogy: allow to deny removal request for comment in moderation 2019-06-20 13:19:35 +02:00
Antoine Bartuccio 04009a6a5b
pedagogy: moderation interface 2019-06-20 12:15:12 +02:00
Antoine Bartuccio 3d0f5c0a15
pedagogy: base for uv comment moderation 2019-06-20 01:29:12 +02:00
Antoine Bartuccio 437af4dd04 Merge branch 'pedagogy_v2_search_api' into 'pedagogy_v2'
Pedagogy v2 search api

See merge request ae/Sith!214
2019-06-19 10:36:27 +02:00
Antoine Bartuccio ba61455017 Merge branch 'pedagogy_v2_old_base_import' into 'pedagogy_v2'
Pedagogy v2 old base import

See merge request ae/Sith!213
2019-06-19 10:35:50 +02:00
Antoine Bartuccio 624f1d653d
pedagogy: tests for search API 2019-06-19 10:04:29 +02:00
Antoine Bartuccio e21821ace5
pedagogy: handle one letter search 2019-06-19 02:00:00 +02:00
Antoine Bartuccio 22c028af11
pedagogy: rename query to search in search API 2019-06-19 01:53:02 +02:00
Antoine Bartuccio f0560f0d2a
pedagogy: fix import for HUMA on old database 2019-06-19 01:49:34 +02:00
Antoine Bartuccio 502ae09523
pedagogy: add filters to search api 2019-06-19 01:26:11 +02:00
Antoine Bartuccio 2cbef2babc
pedagogy: support json response from search API 2019-06-19 00:58:20 +02:00
Antoine Bartuccio e11d45b51e
pedagogy: more details on uv_detail for tests purpose 2019-06-19 00:58:14 +02:00
Antoine Bartuccio 061320a5df
pedagogy: search index for uvs and search api 2019-06-19 00:57:55 +02:00
Antoine Bartuccio 2aa465b138
pedagogy: don't update uv comment publish date at each save 2019-06-19 00:35:44 +02:00
Antoine Bartuccio d18f0aa829
pedagogy: import results from old uv guide 2019-06-18 17:46:46 +02:00
Antoine Bartuccio e7b8ddb631
pedagogy: importation from old uv guide 2019-06-18 17:20:10 +02:00
Antoine Bartuccio 358a625cc4
pedagogy: simplify and implement department system according to old database model 2019-06-18 10:56:05 +02:00
Antoine Bartuccio d44fa73b2a
pedagogy: Fix grade range on UVCOmment 2019-06-17 18:42:33 +02:00
Antoine Bartuccio 5ccb499665
pedagogy: full test suite for UVComment 2019-06-16 20:05:53 +02:00
Antoine Bartuccio c467165bf3
pedagogy: fix error with author change on comment when edited by an admin 2019-06-16 18:34:11 +02:00
Antoine Bartuccio 8512f3c5d0
pedagogy: fix some previous tests never launched and test for display/create/delete of UVComment 2019-06-16 18:26:30 +02:00
Antoine Bartuccio 5003e57338
pedagogy: functional but basic uv comment system 2019-06-16 17:02:45 +02:00
Antoine Bartuccio b7c2da53fe
pedagogy: models for UVComment and UVResult 2019-06-16 16:02:27 +02:00
Antoine Bartuccio 598cdc0284
pedagogy: tests for uv deletion and update 2019-06-16 13:10:27 +02:00
Antoine Bartuccio 692d9a25e3
pedagogy: tests for uv display 2019-06-16 12:44:55 +02:00
Antoine Bartuccio 38f6c27983
pedagogy: tests for uv creation 2019-06-16 12:19:04 +02:00
Antoine Bartuccio 1172402166
pedagogy: basic uv detail view 2019-06-16 02:19:56 +02:00
Antoine Bartuccio ab344ba02f
pedagogy: complete CRUD for UV model 2019-06-16 00:29:46 +02:00
Antoine Bartuccio ec33311715
pedagogy: basic display list of UV 2019-06-15 23:31:31 +02:00
Antoine Bartuccio 5bf5d0277c
pedagogy: create view and form for UV
WARNING: A new group has been created, to be set by the infra team at deployment !!!
2019-06-15 17:01:25 +02:00
Antoine Bartuccio 31f6ee9ca4
pedagogy: create first iteration of UV model 2019-06-15 14:17:49 +02:00
Antoine Bartuccio b49f204e20
pedagogy: more comprehensible urls for development 2019-06-15 12:49:14 +02:00
Antoine Bartuccio 57f2a5c260
pedagogy: rename Study into StudyField 2019-06-15 12:49:13 +02:00
Antoine Bartuccio 36831b4f4a
pedagogy: rename TeachingDepartment into EducationDepartment 2019-06-15 12:49:13 +02:00
Antoine Bartuccio 17a375f89c
pedagogy: structure of the app 2019-06-15 12:49:13 +02:00
Antoine Bartuccio f922ab9272 Merge branch 'bugfix' into 'master'
counter: fix error for stats 500 on PermissionDenied

See merge request ae/Sith!208
2019-06-14 16:32:34 +02:00
Antoine Bartuccio f0524a9f00 counter: fix error for stats 500 on PermissionDenied 2019-05-28 16:39:16 +02:00
Antoine Bartuccio d466d645e6 Merge branch 'eboutic' into 'master'
eboutic: add some help and documentation for the payment system

See merge request ae/Sith!207
2019-05-27 11:42:37 +02:00
198 changed files with 8979 additions and 5226 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
source ./env/bin/activate

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ sith/settings_custom.py
sith/search_indexes/
.coverage
coverage_report/
doc/_build

View File

@ -5,7 +5,7 @@ test:
stage: test
script:
- apt-get update
- apt-get install -y gettext python3-xapian
- apt-get install -y gettext python3-xapian libgraphviz-dev
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
- python -c 'import xapian' # Fail immediately if there is a problem with xapian
@ -15,6 +15,8 @@ test:
- coverage run ./manage.py test
- coverage html
- coverage report
- cd doc
- make html # Make documentation
artifacts:
paths:
- coverage_report/

18
.readthedocs.yml Normal file
View File

@ -0,0 +1,18 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the doc/ directory with Sphinx
sphinx:
configuration: doc/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.6
install:
- requirements: requirements.txt

View File

@ -1,106 +0,0 @@
*Contribuer c'est la vie*
=========================
Hey ! Tu veux devenir un mec bien et en plus devenir bon en python si tu l'es pas déjà ?
Il se trouve que le sith AE prévu pour l'été 2016 a besoin de toi !
Pour faire le sith, on utilise le framework Web [Django](https://docs.djangoproject.com/fr/1.11/intro/)
N'hésite pas à lire les tutos et à nous demander (ae.info@utbm.fr).
Bon, passons aux choses sérieuses, pour bidouiller le sith sans le casser :
Ben en fait, tu peux pas le casser, tu vas juste t'amuser comme un petit fou sur un clone du sith.
C'est pas compliqué, il suffit d'avoir [Git](http://www.git-scm.com/book/fr/v2), python et pip (pour faciliter la gestion des paquets python).
Tout d'abord, tu vas avoir besoin d'un compte Gitlab pour pouvoir te connecter.
Ensuite, tu fais :
`git clone https://ae-dev.utbm.fr/ae/Sith.git`
Avec cette commande, tu clones le sith AE dans le dossier courant.
```bash
cd Sith
virtualenv --system-site-packages --python=python3 env
source env/bin/activate
pip install -r requirements.txt
./manage runserver
```
Attention aux dépendances système, à voir dans le README.md
Maintenant, faut passer le sith en mode debug dans le fichier de settings personnalisé.
```bash
echo "DEBUG=True" > sith/settings_custom.py
echo 'SITH_URL = "localhost:8000"' >> sith/settings_custom.py
```
Enfin, il s'agit de créer la base de donnée de test lors de la première utilisation
```bash
./manage.py setup
```
Et pour lancer le sith, tu fais `python3 manage.py runserver`
Voilà, c'est le sith AE. Il y a des issues dans le gitlab qui sont à régler. Si tu as un domaine qui t'intéresse, une appli que tu voudrais développer, n'hésites pas et contacte-nous.
Va, et que l'AE soit avec toi.
# Black
Pour uniformiser le formattage du code nous utilisons [Black](https://github.com/ambv/black). Cela permet d'avoir le même codestyle et donc le codereview prend moins de temps. Tout étant dans le même format, il est plus facile pour chacun de comprendre le code de chacun ! Cela permet aussi d'éviter des erreurs (y parait 🤷‍♀️).
Installation de black:
```bash
pip install black
```
## Sous VsCode:
Attention, pour VsCode, Black doit être installé dans votre virtualenv !
Ajouter ces deux lignes dans les settings de VsCode
```json
{
"python.formatting.provider": "black",
"editor.formatOnSave": true
}
```
## Sous Sublime Text
Il faut installer le plugin [sublack](https://packagecontrol.io/packages/sublack) depuis Package Control.
Il suffit ensuite d'ajouter dans les settings du projet (ou en global)
```json
{
"sublack.black_on_save": true
}
```
Si vous utilisez le plugin [anaconda](http://damnwidget.github.io/anaconda/), pensez à modifier les paramètres du linter pep8 pour éviter de recevoir des warnings dans le formatage de black
```json
{
"pep8_ignore": [
"E203",
"E266",
"E501",
"W503"
]
}
```
Sites et doc cools
------------------
[Classy Class-Based Views](http://ccbv.co.uk/projects/Django/1.11/)
Helpers:
`./manage.py makemessages --ignore "env/*" -e py,jinja`
`for f in $(find . -name "*.py" ! -path "*migration*" ! -path "./env/*" ! -path "./doc/*"); do cat ./doc/header "$f" > /tmp/temp && mv /tmp/temp "$f"; done`

3
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,3 @@
Pour contribuer au projet, vous pouvez vous référer à la documentation disponible à https://sith-ae.readthedocs.io/.
Et n'oubliez pas, contribuer c'est la vie !

2320
Doxyfile

File diff suppressed because it is too large Load Diff

104
README.md
View File

@ -1,104 +0,0 @@
[![pipeline status](https://ae-dev.utbm.fr/ae/Sith/badges/master/pipeline.svg)](https://ae-dev.utbm.fr/ae/Sith/commits/master)
[![coverage report](https://ae-dev.utbm.fr/ae/Sith/badges/master/coverage.svg)](https://ae-dev.utbm.fr/ae/Sith/commits/master)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://ae-dev.zulipchat.com)
## Sith AE
### Dependencies:
See requirements.txt
You may need to install some dev libraries like `libmysqlclient-dev`, `libssl-dev`, `libjpeg-dev`, `python3-xapian`, or `zlib1g-dev` to install all the
requiered dependancies with pip. You may also need `mysql-client`. Don't also forget `python3-dev` if you don't have it
already.
You can check all of them with:
```bash
sudo apt install libmysqlclient-dev libssl-dev libjpeg-dev zlib1g-dev python3-dev libffi-dev python3-dev libgraphviz-dev pkg-config python3-xapian gettext
```
On macos, you will need homebrew
```bash
brew install xapian
```
If it doesn't work it's because it need [this pull request](https://github.com/Homebrew/homebrew-core/pull/34835) to be validated.
The development is done with sqlite, but it is advised to set a more robust DBMS for production (Postgresql for example)
### Get started
To start working on the project, just run the following commands:
```bash
git clone https://ae-dev.utbm.fr/ae/Sith.git
cd Sith
virtualenv --system-site-packages --python=python3 env
source env/bin/activate
pip install -r requirements.txt
./manage.py setup
```
To start the simple development server, just run `python3 manage.py runserver`
For more informations, check out the CONTRIBUTING.md file.
### Logging errors with sentry
To connect the app to sentry.io, you must set the variable SENTRY_DSN in your settings custom. It's composed of the full link given on your sentry project
### Generating documentation
There is a Doxyfile at the root of the project, meaning that if you have Doxygen, you can run `doxygen Doxyfile` to
generate a complete HTML documentation that will be available in the *./doc/html/* folder.
### Collecting statics for production:
We use scss in the project. In development environment (DEBUG=True), scss is compiled every time the file is needed. For production, it assumes you have already compiled every files and to do so, you need to use the following commands :
```bash
./manage.py collectstatic # To collect statics
./manage.py compilestatic # To compile scss in those statics
```
### Misc about development
#### Controlling the rights
When you need to protect an object, there are three levels:
* Editing the object properties
* Editing the object various values
* Viewing the object
Now you have many solutions in your model:
* You can define a `is_owned_by(self, user)`, a `can_be_edited_by(self, user)`, and/or a `can_be_viewed_by(self, user)`
method, each returning True is the user passed can edit/view the object, False otherwise.
This allows you to make complex request when the group solution is not powerful enough.
It's useful too when you want to define class-wide permissions, e.g. the club members, that are viewable only for
Subscribers.
* You can add an `owner_group` field, as a ForeignKey to Group. Second is an `edit_groups` field, as a ManyToMany to
Group, and third is a `view_groups`, same as for edit.
Finally, when building a class based view, which is highly advised, you just have to inherit it from CanEditPropMixin,
CanEditMixin, or CanViewMixin, which are located in core.views. Your view will then be protected using either the
appropriate group fields, or the right method to check user permissions.
#### Counting the number of line of code
```bash
sudo apt install cloc
cloc --exclude-dir=doc,env .
```
#### Updating doc/SYNTAX.md
If you make an update in the Markdown syntax parser, it's good to document
update the syntax reference page in `doc/SYNTAX.md`. But updating this file will
break the tests if you don't update the corresponding `doc/SYNTAX.html` file at
the same time.
To do that, simply run `./manage.py markdown > doc/SYNTAX.html`,
and the tests should pass again.

37
README.rst Normal file
View File

@ -0,0 +1,37 @@
.. image:: https://ae-dev.utbm.fr/ae/Sith/badges/master/pipeline.svg
:target: https://ae-dev.utbm.fr/ae/Sith/commits/master
:alt: pipeline status
.. image:: https://readthedocs.org/projects/sith-ae/badge/?version=latest
:target: https://sith-ae.readthedocs.io/?badge=latest
:alt: documentation Status
.. image:: https://ae-dev.utbm.fr/ae/Sith/badges/master/coverage.svg
:target: https://ae-dev.utbm.fr/ae/Sith/commits/master
:alt: coverage report
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: code style: black
.. image:: https://img.shields.io/badge/zulip-join_chat-brightgreen.svg
:target: https://ae-dev.zulipchat.com
:alt: project chat
This is the source code of the UTBM's student association available at https://ae.utbm.fr/.
All documentation is in the ``docs`` directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
If you want to contribute, here's how we recommend to read the docs:
* First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn.
* If in the first part you find you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful.
* Keep in mind that this documentation is thought to be read in order.
To join our team :
* Send a mail at mailto:ae.utbm.fr
* Join our group chat at https://ae-dev.zulipchat.com
* See and join our Trello at https://trello.com/b/YQOaF33m/site-ae.
This project is licenced under GNU GPL, see the LICENSE file at the top of the repository for more details.

View File

@ -1,9 +0,0 @@
# TODO
## Easter eggs
* 'A' 'L' 'L' 'O': Entendre le Allooo de Madame Coucoune
* idem avec cacafe
* Un meat spin quelque part
* Konami code

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators
import accounting.models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -243,6 +244,7 @@ class Migration(migrations.Migration):
verbose_name="accounting type",
to="accounting.AccountingType",
blank=True,
on_delete=django.db.models.deletion.CASCADE,
),
),
],
@ -267,6 +269,7 @@ class Migration(migrations.Migration):
verbose_name="simplified accounting types",
to="accounting.AccountingType",
related_name="simplified_types",
on_delete=django.db.models.deletion.CASCADE,
),
),
],

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -22,6 +23,7 @@ class Migration(migrations.Migration):
verbose_name="invoice",
to="core.SithFile",
blank=True,
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AddField(
@ -31,12 +33,14 @@ class Migration(migrations.Migration):
verbose_name="journal",
to="accounting.GeneralJournal",
related_name="operations",
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AddField(
model_name="operation",
name="linked_operation",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
blank=True,
to="accounting.Operation",
null=True,
@ -54,6 +58,7 @@ class Migration(migrations.Migration):
verbose_name="simple type",
to="accounting.SimplifiedAccountingType",
blank=True,
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AddField(
@ -63,6 +68,7 @@ class Migration(migrations.Migration):
verbose_name="club account",
to="accounting.ClubAccount",
related_name="journals",
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AddField(
@ -72,20 +78,27 @@ class Migration(migrations.Migration):
verbose_name="bank account",
to="accounting.BankAccount",
related_name="club_accounts",
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AddField(
model_name="clubaccount",
name="club",
field=models.ForeignKey(
verbose_name="club", to="club.Club", related_name="club_account"
verbose_name="club",
to="club.Club",
related_name="club_account",
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AddField(
model_name="bankaccount",
name="club",
field=models.ForeignKey(
verbose_name="club", to="club.Club", related_name="bank_accounts"
verbose_name="club",
to="club.Club",
related_name="bank_accounts",
on_delete=django.db.models.deletion.CASCADE,
),
),
migrations.AlterUniqueTogether(

View File

@ -29,6 +29,7 @@ class Migration(migrations.Migration):
related_name="labels",
verbose_name="club account",
to="accounting.ClubAccount",
on_delete=django.db.models.deletion.CASCADE,
),
),
],

View File

@ -22,7 +22,7 @@
#
#
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.exceptions import ValidationError
from django.core import validators
from django.db import models
@ -110,7 +110,12 @@ class BankAccount(models.Model):
name = models.CharField(_("name"), max_length=30)
iban = models.CharField(_("iban"), max_length=255, blank=True)
number = models.CharField(_("account number"), max_length=255, blank=True)
club = models.ForeignKey(Club, related_name="bank_accounts", verbose_name=_("club"))
club = models.ForeignKey(
Club,
related_name="bank_accounts",
verbose_name=_("club"),
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("Bank account")
@ -136,9 +141,17 @@ class BankAccount(models.Model):
class ClubAccount(models.Model):
name = models.CharField(_("name"), max_length=30)
club = models.ForeignKey(Club, related_name="club_account", verbose_name=_("club"))
club = models.ForeignKey(
Club,
related_name="club_account",
verbose_name=_("club"),
on_delete=models.CASCADE,
)
bank_account = models.ForeignKey(
BankAccount, related_name="club_accounts", verbose_name=_("bank account")
BankAccount,
related_name="club_accounts",
verbose_name=_("bank account"),
on_delete=models.CASCADE,
)
class Meta:
@ -203,7 +216,11 @@ class GeneralJournal(models.Model):
name = models.CharField(_("name"), max_length=40)
closed = models.BooleanField(_("is closed"), default=False)
club_account = models.ForeignKey(
ClubAccount, related_name="journals", null=False, verbose_name=_("club account")
ClubAccount,
related_name="journals",
null=False,
verbose_name=_("club account"),
on_delete=models.CASCADE,
)
amount = CurrencyField(_("amount"), default=0)
effective_amount = CurrencyField(_("effective_amount"), default=0)
@ -263,7 +280,11 @@ class Operation(models.Model):
number = models.IntegerField(_("number"))
journal = models.ForeignKey(
GeneralJournal, related_name="operations", null=False, verbose_name=_("journal")
GeneralJournal,
related_name="operations",
null=False,
verbose_name=_("journal"),
on_delete=models.CASCADE,
)
amount = CurrencyField(_("amount"))
date = models.DateField(_("date"))
@ -282,6 +303,7 @@ class Operation(models.Model):
verbose_name=_("invoice"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
done = models.BooleanField(_("is done"), default=False)
simpleaccounting_type = models.ForeignKey(
@ -290,6 +312,7 @@ class Operation(models.Model):
verbose_name=_("simple type"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
accounting_type = models.ForeignKey(
"AccountingType",
@ -297,6 +320,7 @@ class Operation(models.Model):
verbose_name=_("accounting type"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
label = models.ForeignKey(
"Label",
@ -328,6 +352,7 @@ class Operation(models.Model):
null=True,
blank=True,
default=None,
on_delete=models.CASCADE,
)
class Meta:
@ -487,6 +512,7 @@ class SimplifiedAccountingType(models.Model):
AccountingType,
related_name="simplified_types",
verbose_name=_("simplified accounting types"),
on_delete=models.CASCADE,
)
class Meta:
@ -518,7 +544,10 @@ class Label(models.Model):
name = models.CharField(_("label"), max_length=64)
club_account = models.ForeignKey(
ClubAccount, related_name="labels", verbose_name=_("club account")
ClubAccount,
related_name="labels",
verbose_name=_("club account"),
on_delete=models.CASCADE,
)
class Meta:

View File

@ -20,14 +20,14 @@
{% for k,v in statement.items() %}
<tr>
<td>{{ k }}</td>
<td>{{ v }}</td>
<td>{{ "%.2f" % v }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} €</p>
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
</div>
{% endblock %}

View File

@ -18,12 +18,12 @@
{% for k,v in dict['CREDIT'].items() %}
<tr>
<td>{{ k }}</td>
<td>{{ v }}</td>
<td>{{ "%.2f" % v }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% trans %}Total: {% endtrans %}{{ dict['CREDIT_sum'] }}
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }}
<h6>{% trans %}Debit{% endtrans %}</h6>
<table>
@ -37,19 +37,19 @@
{% for k,v in dict['DEBIT'].items() %}
<tr>
<td>{{ k }}</td>
<td>{{ v }}</td>
<td>{{ "%.2f" % v }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% trans %}Total: {% endtrans %}{{ dict['DEBIT_sum'] }}
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
{% endmacro %}
{% block content %}
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
{% for k,v in statement.items() %}
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ v['CREDIT_sum'] - v['DEBIT_sum'] }}</h4>
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4>
{{ display_tables(v) }}
<hr>
{% endfor %}

View File

@ -28,14 +28,14 @@
{% else %}
<td></td>
{% endif %}
<td>{{ credit_statement[key] }}</td>
<td>{{ "%.2f" % credit_statement[key] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Total : {{ total_credit }}</p>
<p>Total : {{ "%.2f" % total_credit }}</p>
<h4>{% trans %}Debit{% endtrans %}</h4>
@ -56,13 +56,13 @@
{% else %}
<td></td>
{% endif %}
<td>{{ debit_statement[key] }}</td>
<td>{{ "%.2f" % debit_statement[key] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Total : {{ total_debit }}</p>
<p>Total : {{ "%.2f" % total_debit }}</p>
</div>
{% endblock %}

View File

@ -23,7 +23,7 @@
#
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.management import call_command
from datetime import date
@ -272,30 +272,50 @@ class OperationTest(TestCase):
def test_nature_statement(self):
self.client.login(username="comptable", password="plop")
response_get = self.client.get(
response = self.client.get(
reverse("accounting:journal_nature_statement", args=[self.journal.id])
)
self.assertTrue(
"bob (Troll Pench\\xc3\\xa9) : 3.00" in str(response_get.content)
)
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
def test_person_statement(self):
self.client.login(username="comptable", password="plop")
response_get = self.client.get(
response = self.client.get(
reverse("accounting:journal_person_statement", args=[self.journal.id])
)
self.assertTrue(
"<td>3.00</td>" in str(response_get.content)
and '<td><a href="/user/1/">S&#39; Kia</a></td>'
in str(response_get.content)
self.assertContains(response, "Total : 5575.72", status_code=200)
self.assertContains(response, "Total : 71.42")
self.assertContains(
response,
"""
<td><a href="/user/1/">S&#39; Kia</a></td>
<td>3.00</td>""",
)
self.assertContains(
response,
"""
<td><a href="/user/1/">S&#39; Kia</a></td>
<td>823.00</td>""",
)
def test_accounting_statement(self):
self.client.login(username="comptable", password="plop")
response_get = self.client.get(
response = self.client.get(
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
)
self.assertTrue(
"<td>443 - Cr\\xc3\\xa9dit - Ce code n&#39;existe pas</td>"
in str(response_get.content)
self.assertContains(
response,
"""
<tr>
<td>443 - Crédit - Ce code n&#39;existe pas</td>
<td>3.00</td>
</tr>""",
status_code=200,
)
self.assertContains(
response,
"""
<p><strong>Montant : </strong>-5504.30 </p>
<p><strong>Montant effectif: </strong>-5504.30 </p>""",
)

View File

@ -22,131 +22,133 @@
#
#
from django.conf.urls import url
from django.urls import re_path
from accounting.views import *
urlpatterns = [
# Accounting types
url(
re_path(
r"^simple_type$",
SimplifiedAccountingTypeListView.as_view(),
name="simple_type_list",
),
url(
re_path(
r"^simple_type/create$",
SimplifiedAccountingTypeCreateView.as_view(),
name="simple_type_new",
),
url(
re_path(
r"^simple_type/(?P<type_id>[0-9]+)/edit$",
SimplifiedAccountingTypeEditView.as_view(),
name="simple_type_edit",
),
# Accounting types
url(r"^type$", AccountingTypeListView.as_view(), name="type_list"),
url(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"),
url(
re_path(r"^type$", AccountingTypeListView.as_view(), name="type_list"),
re_path(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"),
re_path(
r"^type/(?P<type_id>[0-9]+)/edit$",
AccountingTypeEditView.as_view(),
name="type_edit",
),
# Bank accounts
url(r"^$", BankAccountListView.as_view(), name="bank_list"),
url(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"),
url(
re_path(r"^$", BankAccountListView.as_view(), name="bank_list"),
re_path(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"),
re_path(
r"^bank/(?P<b_account_id>[0-9]+)$",
BankAccountDetailView.as_view(),
name="bank_details",
),
url(
re_path(
r"^bank/(?P<b_account_id>[0-9]+)/edit$",
BankAccountEditView.as_view(),
name="bank_edit",
),
url(
re_path(
r"^bank/(?P<b_account_id>[0-9]+)/delete$",
BankAccountDeleteView.as_view(),
name="bank_delete",
),
# Club accounts
url(r"^club/create$", ClubAccountCreateView.as_view(), name="club_new"),
url(
re_path(r"^club/create$", ClubAccountCreateView.as_view(), name="club_new"),
re_path(
r"^club/(?P<c_account_id>[0-9]+)$",
ClubAccountDetailView.as_view(),
name="club_details",
),
url(
re_path(
r"^club/(?P<c_account_id>[0-9]+)/edit$",
ClubAccountEditView.as_view(),
name="club_edit",
),
url(
re_path(
r"^club/(?P<c_account_id>[0-9]+)/delete$",
ClubAccountDeleteView.as_view(),
name="club_delete",
),
# Journals
url(r"^journal/create$", JournalCreateView.as_view(), name="journal_new"),
url(
re_path(r"^journal/create$", JournalCreateView.as_view(), name="journal_new"),
re_path(
r"^journal/(?P<j_id>[0-9]+)$",
JournalDetailView.as_view(),
name="journal_details",
),
url(
re_path(
r"^journal/(?P<j_id>[0-9]+)/edit$",
JournalEditView.as_view(),
name="journal_edit",
),
url(
re_path(
r"^journal/(?P<j_id>[0-9]+)/delete$",
JournalDeleteView.as_view(),
name="journal_delete",
),
url(
re_path(
r"^journal/(?P<j_id>[0-9]+)/statement/nature$",
JournalNatureStatementView.as_view(),
name="journal_nature_statement",
),
url(
re_path(
r"^journal/(?P<j_id>[0-9]+)/statement/person$",
JournalPersonStatementView.as_view(),
name="journal_person_statement",
),
url(
re_path(
r"^journal/(?P<j_id>[0-9]+)/statement/accounting$",
JournalAccountingStatementView.as_view(),
name="journal_accounting_statement",
),
# Operations
url(
re_path(
r"^operation/create/(?P<j_id>[0-9]+)$",
OperationCreateView.as_view(),
name="op_new",
),
url(r"^operation/(?P<op_id>[0-9]+)$", OperationEditView.as_view(), name="op_edit"),
url(
re_path(
r"^operation/(?P<op_id>[0-9]+)$", OperationEditView.as_view(), name="op_edit"
),
re_path(
r"^operation/(?P<op_id>[0-9]+)/pdf$", OperationPDFView.as_view(), name="op_pdf"
),
# Companies
url(r"^company/list$", CompanyListView.as_view(), name="co_list"),
url(r"^company/create$", CompanyCreateView.as_view(), name="co_new"),
url(r"^company/(?P<co_id>[0-9]+)$", CompanyEditView.as_view(), name="co_edit"),
re_path(r"^company/list$", CompanyListView.as_view(), name="co_list"),
re_path(r"^company/create$", CompanyCreateView.as_view(), name="co_new"),
re_path(r"^company/(?P<co_id>[0-9]+)$", CompanyEditView.as_view(), name="co_edit"),
# Labels
url(r"^label/new$", LabelCreateView.as_view(), name="label_new"),
url(
re_path(r"^label/new$", LabelCreateView.as_view(), name="label_new"),
re_path(
r"^label/(?P<clubaccount_id>[0-9]+)$",
LabelListView.as_view(),
name="label_list",
),
url(
re_path(
r"^label/(?P<label_id>[0-9]+)/edit$", LabelEditView.as_view(), name="label_edit"
),
url(
re_path(
r"^label/(?P<label_id>[0-9]+)/delete$",
LabelDeleteView.as_view(),
name="label_delete",
),
# User account
url(r"^refound/account$", RefoundAccountView.as_view(), name="refound_account"),
re_path(r"^refound/account$", RefoundAccountView.as_view(), name="refound_account"),
]

View File

@ -24,7 +24,7 @@
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, CreateView, DeleteView, FormView
from django.core.urlresolvers import reverse_lazy, reverse
from django.urls import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied, ValidationError

View File

@ -22,35 +22,36 @@
#
#
from django.conf.urls import url, include
from django.urls import re_path, include
from api.views import *
from rest_framework import routers
# Router config
router = routers.DefaultRouter()
router.register(r"counter", CounterViewSet, base_name="api_counter")
router.register(r"user", UserViewSet, base_name="api_user")
router.register(r"club", ClubViewSet, base_name="api_club")
router.register(r"group", GroupViewSet, base_name="api_group")
router.register(r"counter", CounterViewSet, basename="api_counter")
router.register(r"user", UserViewSet, basename="api_user")
router.register(r"club", ClubViewSet, basename="api_club")
router.register(r"group", GroupViewSet, basename="api_group")
# Launderette
router.register(
r"launderette/place", LaunderettePlaceViewSet, base_name="api_launderette_place"
r"launderette/place", LaunderettePlaceViewSet, basename="api_launderette_place"
)
router.register(
r"launderette/machine",
LaunderetteMachineViewSet,
base_name="api_launderette_machine",
basename="api_launderette_machine",
)
router.register(
r"launderette/token", LaunderetteTokenViewSet, base_name="api_launderette_token"
r"launderette/token", LaunderetteTokenViewSet, basename="api_launderette_token"
)
urlpatterns = [
# API
url(r"^", include(router.urls)),
url(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
url(r"^markdown$", RenderMarkdown, name="api_markdown"),
url(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
re_path(r"^", include(router.urls)),
re_path(r"^login/", include("rest_framework.urls", namespace="rest_framework")),
re_path(r"^markdown$", RenderMarkdown, name="api_markdown"),
re_path(r"^mailings$", FetchMailingLists, name="mailings_fetch"),
re_path(r"^uv$", uv_endpoint, name="uv_endpoint"),
]

View File

@ -25,7 +25,7 @@
from rest_framework.response import Response
from rest_framework import viewsets
from django.core.exceptions import PermissionDenied
from rest_framework.decorators import detail_route
from rest_framework.decorators import action
from django.db.models.query import QuerySet
from core.views import can_view, can_edit
@ -46,7 +46,7 @@ def check_if(obj, user, test):
class ManageModelMixin:
@detail_route()
@action(detail=True)
def id(self, request, pk=None):
"""
Get by id (api/v1/router/{pk}/id/)
@ -77,3 +77,4 @@ from .user import *
from .club import *
from .group import *
from .launderette import *
from .uv import *

View File

@ -25,7 +25,6 @@
from rest_framework.response import Response
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer
from rest_framework.views import APIView
from core.templatetags.renderer import markdown

View File

@ -24,7 +24,7 @@
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import list_route
from rest_framework.decorators import action
from counter.models import Counter
@ -51,7 +51,7 @@ class CounterViewSet(RightModelViewSet):
serializer_class = CounterSerializer
queryset = Counter.objects.all()
@list_route()
@action(detail=False)
def bar(self, request):
"""
Return all bars (api/v1/counter/bar/)

View File

@ -24,7 +24,7 @@
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import list_route
from rest_framework.decorators import action
from launderette.models import Launderette, Machine, Token
@ -96,7 +96,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
serializer_class = LaunderetteTokenSerializer
queryset = Token.objects.all()
@list_route()
@action(detail=False)
def washing(self, request):
"""
Return all washing tokens (api/v1/launderette/token/washing)
@ -105,7 +105,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
@list_route()
@action(detail=False)
def drying(self, request):
"""
Return all drying tokens (api/v1/launderette/token/drying)
@ -114,7 +114,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
@list_route()
@action(detail=False)
def avaliable(self, request):
"""
Return all avaliable tokens (api/v1/launderette/token/avaliable)
@ -125,7 +125,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
serializer = self.get_serializer(self.queryset, many=True)
return Response(serializer.data)
@list_route()
@action(detail=False)
def unavaliable(self, request):
"""
Return all unavaliable tokens (api/v1/launderette/token/unavaliable)

View File

@ -26,7 +26,7 @@ import datetime
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import list_route
from rest_framework.decorators import action
from core.models import User
@ -57,7 +57,7 @@ class UserViewSet(RightModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.filter(is_active=True)
@list_route()
@action(detail=False)
def birthday(self, request):
"""
Return all users born today (api/v1/user/birstdays)

127
api/views/uv.py Normal file
View File

@ -0,0 +1,127 @@
from rest_framework.response import Response
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import JSONRenderer
from django.core.exceptions import PermissionDenied
from django.conf import settings
from rest_framework import serializers
import urllib.request
import json
from pedagogy.views import CanCreateUVFunctionMixin
@api_view(["GET"])
@renderer_classes((JSONRenderer,))
def uv_endpoint(request):
if not CanCreateUVFunctionMixin.can_create_uv(request.user):
raise PermissionDenied
params = request.query_params
if "year" not in params or "code" not in params:
raise serializers.ValidationError("Missing query parameter")
short_uv, full_uv = find_uv("fr", params["year"], params["code"])
if short_uv is None or full_uv is None:
return Response(status=204)
return Response(make_clean_uv(short_uv, full_uv))
def find_uv(lang, year, code):
"""
Uses the UTBM API to find an UV.
short_uv is the UV entry in the UV list. It is returned as it contains
information which are not in full_uv.
full_uv is the detailed representation of an UV.
"""
# query the UV list
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
response = urllib.request.urlopen(uvs_url)
uvs = json.loads(response.read().decode("utf-8"))
try:
# find the first UV which matches the code
short_uv = next(uv for uv in uvs if uv["code"] == code)
except StopIteration:
return (None, None)
# get detailed information about the UV
uv_url = settings.SITH_PEDAGOGY_UTBM_API + "/uv/{}/{}/{}/{}".format(
lang, year, code, short_uv["codeFormation"]
)
response = urllib.request.urlopen(uv_url)
full_uv = json.loads(response.read().decode("utf-8"))
return (short_uv, full_uv)
def make_clean_uv(short_uv, full_uv):
"""
Cleans the data up so that it corresponds to our data representation.
"""
res = {}
res["credit_type"] = short_uv["codeCategorie"]
# probably wrong on a few UVs as we pick the first UV we find but
# availability depends on the formation
semesters = {
(True, True): "AUTUMN_AND_SPRING",
(True, False): "AUTUMN",
(False, True): "SPRING",
}
res["semester"] = semesters.get(
(short_uv["ouvertAutomne"], short_uv["ouvertPrintemps"]), "CLOSED"
)
langs = {"es": "SP", "en": "EN", "de": "DE"}
res["language"] = langs.get(full_uv["codeLangue"], "FR")
if full_uv["departement"] == "Pôle Humanités":
res["department"] = "HUMA"
else:
departments = {
"AL": "IMSI",
"AE": "EE",
"GI": "GI",
"GC": "EE",
"GM": "MC",
"TC": "TC",
"GP": "IMSI",
"ED": "EDIM",
"AI": "GI",
"AM": "MC",
}
res["department"] = departments.get(full_uv["codeFormation"], "NA")
res["credits"] = full_uv["creditsEcts"]
activities = ("CM", "TD", "TP", "THE", "TE")
for activity in activities:
res["hours_{}".format(activity)] = 0
for activity in full_uv["activites"]:
if activity["code"] in activities:
res["hours_{}".format(activity["code"])] += activity["nbh"] // 60
# wrong if the manager changes depending on the semester
semester = full_uv.get("automne", None)
if not semester:
semester = full_uv.get("printemps", {})
res["manager"] = semester.get("responsable", "")
res["title"] = full_uv["libelle"]
descriptions = {
"objectives": "objectifs",
"program": "programme",
"skills": "acquisitionCompetences",
"key_concepts": "acquisitionNotions",
}
for res_key, full_uv_key in descriptions.items():
res[res_key] = full_uv[full_uv_key]
# if not found or the API did not return a string
if type(res[res_key]) != str:
res[res_key] = ""
return res

View File

@ -66,7 +66,7 @@ class MailingForm(forms.Form):
super(MailingForm, self).__init__(*args, **kwargs)
self.fields["action"] = forms.TypedChoiceField(
(
choices=(
(self.ACTION_NEW_MAILING, _("New Mailing")),
(self.ACTION_NEW_SUBSCRIPTION, _("Subscribe")),
(self.ACTION_REMOVE_SUBSCRIPTION, _("Remove")),
@ -159,13 +159,13 @@ class MailingForm(forms.Form):
class SellingsFormBase(forms.Form):
begin_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import django.core.validators
import django.db.models.deletion
class Migration(migrations.Migration):
@ -90,7 +91,10 @@ class Migration(migrations.Migration):
(
"club",
models.ForeignKey(
verbose_name="club", to="club.Club", related_name="members"
verbose_name="club",
to="club.Club",
related_name="members",
on_delete=django.db.models.deletion.CASCADE,
),
),
],

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -18,6 +19,7 @@ class Migration(migrations.Migration):
model_name="membership",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="user",
to=settings.AUTH_USER_MODEL,
related_name="membership",
@ -34,6 +36,7 @@ class Migration(migrations.Migration):
model_name="club",
name="home",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
blank=True,
null=True,
related_name="home_of_club",
@ -45,14 +48,21 @@ class Migration(migrations.Migration):
model_name="club",
name="owner_group",
field=models.ForeignKey(
default=1, to="core.Group", related_name="owned_club"
on_delete=django.db.models.deletion.CASCADE,
default=1,
to="core.Group",
related_name="owned_club",
),
),
migrations.AddField(
model_name="club",
name="parent",
field=models.ForeignKey(
null=True, to="club.Club", related_name="children", blank=True
on_delete=django.db.models.deletion.CASCADE,
null=True,
to="club.Club",
related_name="children",
blank=True,
),
),
migrations.AddField(

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
model_name="membership",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="user",
related_name="memberships",
to=settings.AUTH_USER_MODEL,

View File

@ -5,6 +5,7 @@ from django.db import migrations, models
from django.conf import settings
import re
import django.core.validators
import django.db.models.deletion
class Migration(migrations.Migration):
@ -51,12 +52,16 @@ class Migration(migrations.Migration):
(
"club",
models.ForeignKey(
verbose_name="Club", related_name="mailings", to="club.Club"
on_delete=django.db.models.deletion.CASCADE,
verbose_name="Club",
related_name="mailings",
to="club.Club",
),
),
(
"moderator",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
null=True,
verbose_name="moderator",
related_name="moderated_mailings",
@ -84,6 +89,7 @@ class Migration(migrations.Migration):
(
"mailing",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="Mailing",
related_name="subscriptions",
to="club.Mailing",
@ -92,6 +98,7 @@ class Migration(migrations.Migration):
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
null=True,
verbose_name="User",
related_name="mailing_subscriptions",

View File

@ -5,6 +5,7 @@ from django.db import migrations, models
from club.models import Club
from core.operations import PsqlRunOnly
import django.db.models.deletion
def generate_club_pages(apps, schema_editor):
@ -31,7 +32,11 @@ class Migration(migrations.Migration):
model_name="club",
name="page",
field=models.OneToOneField(
related_name="club", blank=True, null=True, to="core.Page"
on_delete=django.db.models.deletion.CASCADE,
related_name="club",
blank=True,
null=True,
to="core.Page",
),
),
migrations.AddField(

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import club.models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
model_name="club",
name="owner_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
default=club.models.Club.get_default_owner_group,
related_name="owned_club",
to="core.Group",

View File

@ -29,7 +29,7 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db import transaction
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.utils import timezone
from django.core.validators import RegexValidator, validate_email
from django.utils.functional import cached_property
@ -46,7 +46,9 @@ class Club(models.Model):
id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_("name"), max_length=64)
parent = models.ForeignKey("Club", related_name="children", null=True, blank=True)
parent = models.ForeignKey(
"Club", related_name="children", null=True, blank=True, on_delete=models.CASCADE
)
unix_name = models.CharField(
_("unix name"),
max_length=30,
@ -75,7 +77,10 @@ class Club(models.Model):
return settings.SITH_GROUP_ROOT_ID
owner_group = models.ForeignKey(
Group, related_name="owned_club", default=get_default_owner_group
Group,
related_name="owned_club",
default=get_default_owner_group,
on_delete=models.CASCADE,
)
edit_groups = models.ManyToManyField(
Group, related_name="editable_club", blank=True
@ -91,7 +96,9 @@ class Club(models.Model):
blank=True,
on_delete=models.SET_NULL,
)
page = models.OneToOneField(Page, related_name="club", blank=True, null=True)
page = models.OneToOneField(
Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE
)
class Meta:
ordering = ["name", "unix_name"]
@ -183,8 +190,8 @@ class Club(models.Model):
name=settings.SITH_MAIN_MEMBERS_GROUP
).first()
self.make_home()
self.home.edit_groups = [board]
self.home.view_groups = [member, subscribers]
self.home.edit_groups.set([board])
self.home.view_groups.set([member, subscribers])
self.home.save()
self.make_page()
@ -261,9 +268,15 @@ class Membership(models.Model):
related_name="memberships",
null=False,
blank=False,
on_delete=models.CASCADE,
)
club = models.ForeignKey(
Club, verbose_name=_("club"), related_name="members", null=False, blank=False
Club,
verbose_name=_("club"),
related_name="members",
null=False,
blank=False,
on_delete=models.CASCADE,
)
start_date = models.DateField(_("start date"), default=timezone.now)
end_date = models.DateField(_("end date"), null=True, blank=True)
@ -317,7 +330,12 @@ class Mailing(models.Model):
"""
club = models.ForeignKey(
Club, verbose_name=_("Club"), related_name="mailings", null=False, blank=False
Club,
verbose_name=_("Club"),
related_name="mailings",
null=False,
blank=False,
on_delete=models.CASCADE,
)
email = models.CharField(
_("Email address"),
@ -334,7 +352,11 @@ class Mailing(models.Model):
)
is_moderated = models.BooleanField(_("is moderated"), default=False)
moderator = models.ForeignKey(
User, related_name="moderated_mailings", verbose_name=_("moderator"), null=True
User,
related_name="moderated_mailings",
verbose_name=_("moderator"),
null=True,
on_delete=models.CASCADE,
)
def clean(self):
@ -409,6 +431,7 @@ class MailingSubscription(models.Model):
related_name="subscriptions",
null=False,
blank=False,
on_delete=models.CASCADE,
)
user = models.ForeignKey(
User,
@ -416,6 +439,7 @@ class MailingSubscription(models.Model):
related_name="mailing_subscriptions",
null=True,
blank=True,
on_delete=models.CASCADE,
)
email = models.EmailField(_("Email address"), blank=False, null=False)

View File

@ -26,7 +26,7 @@ from django.conf import settings
from django.test import TestCase
from django.utils import timezone, html
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.management import call_command
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
@ -641,7 +641,7 @@ class MailingFormTest(TestCase):
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
mde = Mailing.objects.get(email="mde")
response = self.client.post(
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
@ -650,6 +650,11 @@ class MailingFormTest(TestCase):
"subscription_mailing": mde.id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "comunity@git.an")
self.assertContains(response, "richard@git.an")
self.assertContains(response, "krophil@git.an")

View File

@ -23,80 +23,88 @@
#
#
from django.conf.urls import url
from django.urls import re_path
from club.views import *
urlpatterns = [
url(r"^$", ClubListView.as_view(), name="club_list"),
url(r"^new$", ClubCreateView.as_view(), name="club_new"),
url(r"^stats$", ClubStatView.as_view(), name="club_stats"),
url(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"),
url(
re_path(r"^$", ClubListView.as_view(), name="club_list"),
re_path(r"^new$", ClubCreateView.as_view(), name="club_new"),
re_path(r"^stats$", ClubStatView.as_view(), name="club_stats"),
re_path(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"),
re_path(
r"^(?P<club_id>[0-9]+)/rev/(?P<rev_id>[0-9]+)/$",
ClubRevView.as_view(),
name="club_view_rev",
),
url(r"^(?P<club_id>[0-9]+)/hist$", ClubPageHistView.as_view(), name="club_hist"),
url(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"),
url(
re_path(
r"^(?P<club_id>[0-9]+)/hist$", ClubPageHistView.as_view(), name="club_hist"
),
re_path(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"),
re_path(
r"^(?P<club_id>[0-9]+)/edit/page$",
ClubPageEditView.as_view(),
name="club_edit_page",
),
url(
re_path(
r"^(?P<club_id>[0-9]+)/members$", ClubMembersView.as_view(), name="club_members"
),
url(
re_path(
r"^(?P<club_id>[0-9]+)/elderlies$",
ClubOldMembersView.as_view(),
name="club_old_members",
),
url(
re_path(
r"^(?P<club_id>[0-9]+)/sellings$",
ClubSellingView.as_view(),
name="club_sellings",
),
url(
re_path(
r"^(?P<club_id>[0-9]+)/sellings/csv$",
ClubSellingCSVView.as_view(),
name="sellings_csv",
),
url(r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop"),
url(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"),
url(r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"),
url(
re_path(
r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop"
),
re_path(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"),
re_path(
r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"
),
re_path(
r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
MailingAutoGenerationView.as_view(),
name="mailing_generate",
),
url(
re_path(
r"^(?P<mailing_id>[0-9]+)/mailing/delete$",
MailingDeleteView.as_view(),
name="mailing_delete",
),
url(
re_path(
r"^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$",
MailingSubscriptionDeleteView.as_view(),
name="mailing_subscription_delete",
),
url(
re_path(
r"^membership/(?P<membership_id>[0-9]+)/set_old$",
MembershipSetOldView.as_view(),
name="membership_set_old",
),
url(r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"),
url(
re_path(
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"
),
re_path(
r"^(?P<club_id>[0-9]+)/poster/create$",
PosterCreateView.as_view(),
name="poster_create",
),
url(
re_path(
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/edit$",
PosterEditView.as_view(),
name="poster_edit",
),
url(
re_path(
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/delete$",
PosterDeleteView.as_view(),
name="poster_delete",

View File

@ -31,7 +31,7 @@ from django.views.generic.edit import DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView, CreateView
from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.core.urlresolvers import reverse, reverse_lazy
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _t
@ -451,7 +451,7 @@ class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView):
current_tab = "props"
class ClubCreateView(CanEditPropMixin, CreateView):
class ClubCreateView(CanCreateMixin, CreateView):
"""
Create a club (for the Sith admin)
"""
@ -574,7 +574,8 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
except ValidationError as validation_error:
return validation_error
users_to_save.append(sub.save())
sub.save()
users_to_save.append(sub)
if cleaned_data["subscription_email"]:
sub = MailingSubscription(

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -50,6 +51,7 @@ class Migration(migrations.Migration):
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="owned_news",
to=settings.AUTH_USER_MODEL,
verbose_name="author",
@ -58,12 +60,16 @@ class Migration(migrations.Migration):
(
"club",
models.ForeignKey(
related_name="news", to="club.Club", verbose_name="club"
on_delete=django.db.models.deletion.CASCADE,
related_name="news",
to="club.Club",
verbose_name="club",
),
),
(
"moderator",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="moderated_news",
null=True,
to=settings.AUTH_USER_MODEL,
@ -99,7 +105,10 @@ class Migration(migrations.Migration):
(
"news",
models.ForeignKey(
related_name="dates", to="com.News", verbose_name="news_date"
on_delete=django.db.models.deletion.CASCADE,
related_name="dates",
to="com.News",
verbose_name="news_date",
),
),
],

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -56,6 +57,7 @@ class Migration(migrations.Migration):
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="author",
related_name="owned_weekmail_articles",
@ -64,6 +66,7 @@ class Migration(migrations.Migration):
(
"club",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="club.Club",
verbose_name="club",
related_name="weekmail_articles",
@ -72,6 +75,7 @@ class Migration(migrations.Migration):
(
"weekmail",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="com.Weekmail",
verbose_name="weekmail",
related_name="articles",

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -48,12 +49,16 @@ class Migration(migrations.Migration):
(
"club",
models.ForeignKey(
verbose_name="club", related_name="posters", to="club.Club"
on_delete=django.db.models.deletion.CASCADE,
verbose_name="club",
related_name="posters",
to="club.Club",
),
),
(
"moderator",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="moderator",
blank=True,
null=True,

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-08-18 17:00
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("com", "0005_auto_20180318_2227")]
operations = [migrations.RemoveField(model_name="sith", name="index_page")]

View File

@ -28,7 +28,7 @@ from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.core.mail import EmailMultiAlternatives
@ -45,7 +45,6 @@ class Sith(models.Model):
alert_msg = models.TextField(_("alert message"), default="", blank=True)
info_msg = models.TextField(_("info message"), default="", blank=True)
index_page = models.TextField(_("index page"), default="", blank=True)
weekmail_destinations = models.TextField(_("weekmail destinations"), default="")
def is_owned_by(self, user):
@ -72,13 +71,22 @@ class News(models.Model):
type = models.CharField(
_("type"), max_length=16, choices=NEWS_TYPES, default="EVENT"
)
club = models.ForeignKey(Club, related_name="news", verbose_name=_("club"))
club = models.ForeignKey(
Club, related_name="news", verbose_name=_("club"), on_delete=models.CASCADE
)
author = models.ForeignKey(
User, related_name="owned_news", verbose_name=_("author")
User,
related_name="owned_news",
verbose_name=_("author"),
on_delete=models.CASCADE,
)
is_moderated = models.BooleanField(_("is moderated"), default=False)
moderator = models.ForeignKey(
User, related_name="moderated_news", verbose_name=_("moderator"), null=True
User,
related_name="moderated_news",
verbose_name=_("moderator"),
null=True,
on_delete=models.CASCADE,
)
def is_owned_by(self, user):
@ -139,7 +147,12 @@ class NewsDate(models.Model):
we don't have to make copies
"""
news = models.ForeignKey(News, related_name="dates", verbose_name=_("news_date"))
news = models.ForeignKey(
News,
related_name="dates",
verbose_name=_("news_date"),
on_delete=models.CASCADE,
)
start_date = models.DateTimeField(_("start_date"), null=True, blank=True)
end_date = models.DateTimeField(_("end_date"), null=True, blank=True)
@ -150,6 +163,13 @@ class NewsDate(models.Model):
class Weekmail(models.Model):
"""
The weekmail class
:ivar title: Title of the weekmail
:ivar intro: Introduction of the weekmail
:ivar joke: Joke of the week
:ivar protip: Tip of the week
:ivar conclusion: Conclusion of the weekmail
:ivar sent: Track if the weekmail has been sent
"""
title = models.CharField(_("title"), max_length=64, blank=True)
@ -163,6 +183,10 @@ class Weekmail(models.Model):
ordering = ["-id"]
def send(self):
"""
Send the weekmail to all users with the receive weekmail option opt-in.
Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.
"""
dest = [
i[0]
for i in Preferences.objects.filter(receive_weekmail=True).values_list(
@ -184,20 +208,32 @@ class Weekmail(models.Model):
Weekmail().save()
def render_text(self):
"""
Renders a pure text version of the mail for readers without HTML support.
"""
return render(
None, "com/weekmail_renderer_text.jinja", context={"weekmail": self}
).content.decode("utf-8")
def render_html(self):
"""
Renders an HTML version of the mail with images and fancy CSS.
"""
return render(
None, "com/weekmail_renderer_html.jinja", context={"weekmail": self}
).content.decode("utf-8")
def get_banner(self):
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA18.jpg")
"""
Return an absolute link to the banner.
"""
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA19.jpg")
def get_footer(self):
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerA18.jpg")
"""
Return an absolute link to the footer.
"""
return "http://" + settings.SITH_URL + static("com/img/weekmail_footerA19.jpg")
def __str__(self):
return "Weekmail %s (sent: %s) - %s" % (self.id, self.sent, self.title)
@ -208,15 +244,25 @@ class Weekmail(models.Model):
class WeekmailArticle(models.Model):
weekmail = models.ForeignKey(
Weekmail, related_name="articles", verbose_name=_("weekmail"), null=True
Weekmail,
related_name="articles",
verbose_name=_("weekmail"),
null=True,
on_delete=models.CASCADE,
)
title = models.CharField(_("title"), max_length=64)
content = models.TextField(_("content"))
author = models.ForeignKey(
User, related_name="owned_weekmail_articles", verbose_name=_("author")
User,
related_name="owned_weekmail_articles",
verbose_name=_("author"),
on_delete=models.CASCADE,
)
club = models.ForeignKey(
Club, related_name="weekmail_articles", verbose_name=_("club")
Club,
related_name="weekmail_articles",
verbose_name=_("club"),
on_delete=models.CASCADE,
)
rank = models.IntegerField(_("rank"), default=-1)
@ -249,7 +295,11 @@ class Poster(models.Model):
)
file = models.ImageField(_("file"), null=False, upload_to="com/posters")
club = models.ForeignKey(
Club, related_name="posters", verbose_name=_("club"), null=False
Club,
related_name="posters",
verbose_name=_("club"),
null=False,
on_delete=models.CASCADE,
)
screens = models.ManyToManyField(Screen, related_name="posters")
date_begin = models.DateTimeField(blank=False, null=False, default=timezone.now)
@ -264,6 +314,7 @@ class Poster(models.Model):
verbose_name=_("moderator"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
def save(self, *args, **kwargs):

View File

@ -40,22 +40,29 @@
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 birthdays %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
</div>
<div id="left_column" class="news_column">

View File

@ -24,12 +24,37 @@
from django.test import TestCase
from django.conf import settings
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.management import call_command
from django.utils import html
from django.utils.translation import ugettext as _
from core.models import User, RealGroup
class ComAlertTest(TestCase):
def setUp(self):
call_command("populate")
def test_page_is_working(self):
self.client.login(username="comunity", password="plop")
response = self.client.get(reverse("com:alert_edit"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
class ComInfoTest(TestCase):
def setUp(self):
call_command("populate")
def test_page_is_working(self):
self.client.login(username="comunity", password="plop")
response = self.client.get(reverse("com:info_edit"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
class ComTest(TestCase):
def setUp(self):
call_command("populate")
@ -37,7 +62,7 @@ class ComTest(TestCase):
self.com_group = RealGroup.objects.filter(
id=settings.SITH_GROUP_COM_ADMIN_ID
).first()
self.skia.groups = [self.com_group]
self.skia.groups.set([self.com_group])
self.skia.save()
self.client.login(username=self.skia.username, password="plop")
@ -74,3 +99,23 @@ class ComTest(TestCase):
"""<div id="info_box">\\n <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>"""
in str(r.content)
)
def test_birthday_non_subscribed_user(self):
self.client.login(username="guy", password="plop")
response = self.client.get(reverse("core:index"))
self.assertContains(
response,
text=html.escape(
_("You need an up to date subscription to access this content")
),
)
def test_birthday_subscibed_user(self):
response = self.client.get(reverse("core:index"))
self.assertNotContains(
response,
text=html.escape(
_("You need an up to date subscription to access this content")
),
)

View File

@ -22,98 +22,103 @@
#
#
from django.conf.urls import url
from django.urls import re_path
from com.views import *
from club.views import MailingDeleteView
urlpatterns = [
url(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"),
url(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"),
url(r"^sith/edit/index$", IndexEditView.as_view(), name="index_edit"),
url(
re_path(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"),
re_path(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"),
re_path(
r"^sith/edit/weekmail_destinations$",
WeekmailDestinationEditView.as_view(),
name="weekmail_destinations",
),
url(r"^weekmail$", WeekmailEditView.as_view(), name="weekmail"),
url(r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview"),
url(
re_path(r"^weekmail$", WeekmailEditView.as_view(), name="weekmail"),
re_path(
r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview"
),
re_path(
r"^weekmail/new_article$",
WeekmailArticleCreateView.as_view(),
name="weekmail_article",
),
url(
re_path(
r"^weekmail/article/(?P<article_id>[0-9]+)/delete$",
WeekmailArticleDeleteView.as_view(),
name="weekmail_article_delete",
),
url(
re_path(
r"^weekmail/article/(?P<article_id>[0-9]+)/edit$",
WeekmailArticleEditView.as_view(),
name="weekmail_article_edit",
),
url(r"^news$", NewsListView.as_view(), name="news_list"),
url(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"),
url(r"^news/create$", NewsCreateView.as_view(), name="news_new"),
url(
re_path(r"^news$", NewsListView.as_view(), name="news_list"),
re_path(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"),
re_path(r"^news/create$", NewsCreateView.as_view(), name="news_new"),
re_path(
r"^news/(?P<news_id>[0-9]+)/delete$",
NewsDeleteView.as_view(),
name="news_delete",
),
url(
re_path(
r"^news/(?P<news_id>[0-9]+)/moderate$",
NewsModerateView.as_view(),
name="news_moderate",
),
url(r"^news/(?P<news_id>[0-9]+)/edit$", NewsEditView.as_view(), name="news_edit"),
url(r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail"),
url(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
url(
re_path(
r"^news/(?P<news_id>[0-9]+)/edit$", NewsEditView.as_view(), name="news_edit"
),
re_path(
r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail"
),
re_path(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
re_path(
r"^mailings/(?P<mailing_id>[0-9]+)/moderate$",
MailingModerateView.as_view(),
name="mailing_moderate",
),
url(
re_path(
r"^mailings/(?P<mailing_id>[0-9]+)/delete$",
MailingDeleteView.as_view(redirect_page="com:mailing_admin"),
name="mailing_delete",
),
url(r"^poster$", PosterListView.as_view(), name="poster_list"),
url(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"),
url(
re_path(r"^poster$", PosterListView.as_view(), name="poster_list"),
re_path(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"),
re_path(
r"^poster/(?P<poster_id>[0-9]+)/edit$",
PosterEditView.as_view(),
name="poster_edit",
),
url(
re_path(
r"^poster/(?P<poster_id>[0-9]+)/delete$",
PosterDeleteView.as_view(),
name="poster_delete",
),
url(
re_path(
r"^poster/moderate$",
PosterModerateListView.as_view(),
name="poster_moderate_list",
),
url(
re_path(
r"^poster/(?P<object_id>[0-9]+)/moderate$",
PosterModerateView.as_view(),
name="poster_moderate",
),
url(r"^screen$", ScreenListView.as_view(), name="screen_list"),
url(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"),
url(
re_path(r"^screen$", ScreenListView.as_view(), name="screen_list"),
re_path(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"),
re_path(
r"^screen/(?P<screen_id>[0-9]+)/slideshow$",
ScreenSlideshowView.as_view(),
name="screen_slideshow",
),
url(
re_path(
r"^screen/(?P<screen_id>[0-9]+)/edit$",
ScreenEditView.as_view(),
name="screen_edit",
),
url(
re_path(
r"^screen/(?P<screen_id>[0-9]+)/delete$",
ScreenDeleteView.as_view(),
name="screen_delete",

View File

@ -29,7 +29,7 @@ from django.views.generic import ListView, DetailView, View
from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse, reverse_lazy
from django.urls import reverse, reverse_lazy
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
@ -74,14 +74,14 @@ class PosterForm(forms.ModelForm):
widgets = {"screens": forms.CheckboxSelectMultiple}
date_begin = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"),
widget=SelectDateTime,
required=True,
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
)
date_end = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=False,
@ -114,9 +114,6 @@ class ComTabsMixin(TabedViewMixin):
"name": _("Weekmail destinations"),
}
)
tab_list.append(
{"url": reverse("com:index_edit"), "slug": "index", "name": _("Index page")}
)
tab_list.append(
{"url": reverse("com:info_edit"), "slug": "info", "name": _("Info message")}
)
@ -182,14 +179,6 @@ class InfoMsgEditView(ComEditView):
success_url = reverse_lazy("com:info_edit")
class IndexEditView(ComEditView):
form_class = modelform_factory(
Sith, fields=["index_page"], widgets={"index_page": MarkdownInput}
)
current_tab = "index"
success_url = reverse_lazy("com:index_edit")
class WeekmailDestinationEditView(ComEditView):
fields = ["weekmail_destinations"]
current_tab = "weekmail_destinations"
@ -211,19 +200,22 @@ class NewsForm(forms.ModelForm):
}
start_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"),
widget=SelectDateTime,
required=False,
)
end_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=False,
)
until = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"], label=_("Until"), widget=SelectDateTime, required=False
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Until"),
widget=SelectDateTime,
required=False,
)
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
@ -238,7 +230,11 @@ class NewsForm(forms.ModelForm):
self.add_error(
"end_date", ValidationError(_("This field is required."))
)
if self.cleaned_data["start_date"] > self.cleaned_data["end_date"]:
if (
not self.has_error("start_date")
and not self.has_error("end_date")
and self.cleaned_data["start_date"] > self.cleaned_data["end_date"]
):
self.add_error(
"end_date",
ValidationError(
@ -750,7 +746,7 @@ class PosterEditBaseView(UpdateView):
def get_context_data(self, **kwargs):
kwargs = super(PosterEditBaseView, self).get_context_data(**kwargs)
if not self.request.user.is_com_admin:
if hasattr(self, "club"):
kwargs["club"] = self.club
return kwargs

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# -*- coding:utf-8 -*
#
# Copyright 2019
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import os
from django.core.management.commands import compilemessages
class Command(compilemessages.Command):
"""
Wrap call to compilemessages to avoid building whole env
"""
help = """
The usage is the same as the real compilemessages
but it goes into the sith dir first
"""
def handle(self, *args, **options):
os.chdir("sith")
super(Command, self).handle(*args, **options)

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python3
# -*- coding:utf-8 -*
#
# Copyright 2019
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import os
import sys
from http.server import test, CGIHTTPRequestHandler
from django.core.management.base import BaseCommand
# TODO Django 2.2 : implement autoreload following
# https://stackoverflow.com/questions/42907285/django-autoreload-add-watched-file
class Command(BaseCommand):
help = "Generate Sphinx documentation and launch basic server"
default_addr = "127.0.0.1"
default_port = "8080"
def add_arguments(self, parser):
parser.add_argument(
"addrport", nargs="?", help="Optional port number, or ipaddr:port"
)
def handle(self, *args, **kwargs):
os.chdir("doc")
err = os.system("make html")
if err != 0:
self.stdout.write("A build error occured, exiting")
sys.exit(err)
os.chdir("_build/html")
addr = self.default_addr
port = self.default_port
if kwargs["addrport"]:
addrport = kwargs["addrport"].split(":")
addr = addrport[0]
if len(addrport) > 1:
port = addrport[1]
if not port.isnumeric():
self.stdout.write("%s is not a valid port" % (port,))
sys.exit(0)
test(HandlerClass=CGIHTTPRequestHandler, port=int(port), bind=addr)

View File

@ -52,6 +52,7 @@ from counter.models import Customer, ProductType, Product, Counter, Selling, Stu
from com.models import Sith, Weekmail, News, NewsDate
from election.models import Election, Role, Candidature, ElectionList
from forum.models import Forum, ForumTopic
from pedagogy.models import UV
class Command(BaseCommand):
@ -84,6 +85,7 @@ class Command(BaseCommand):
Group(name="Banned to subscribe").save()
Group(name="SAS admin").save()
Group(name="Forum admin").save()
Group(name="Pedagogy admin").save()
self.reset_index("core", "auth")
root = User(
id=0,
@ -140,18 +142,18 @@ class Command(BaseCommand):
g.save()
c = Counter(id=b[0], name=b[1], club=bar_club, type="BAR")
c.save()
c.edit_groups = [g]
c.save()
g.editable_counters.add(c)
g.save()
self.reset_index("counter")
Counter(name="Eboutic", club=main_club, type="EBOUTIC").save()
Counter(name="AE", club=main_club, type="OFFICE").save()
home_root.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()
]
club_root.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()
]
home_root.view_groups.set(
[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()]
)
club_root.view_groups.set(
[Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first()]
)
home_root.save()
club_root.save()
@ -161,7 +163,7 @@ class Command(BaseCommand):
p = Page(name="Index")
p.set_lock(root)
p.save()
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
p.set_lock(root)
p.save()
PageRev(
@ -176,7 +178,7 @@ Welcome to the wiki page!
p = Page(name="services")
p.set_lock(root)
p.save()
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
p.set_lock(root)
PageRev(
page=p,
@ -295,9 +297,13 @@ Welcome to the wiki page!
counter.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
counter.groups = [
Group.objects.filter(id=settings.SITH_GROUP_COUNTER_ADMIN_ID).first().id
]
counter.groups.set(
[
Group.objects.filter(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)
.first()
.id
]
)
counter.save()
# Adding user Comptable
comptable = User(
@ -314,11 +320,13 @@ Welcome to the wiki page!
comptable.view_groups = [
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
]
comptable.groups = [
Group.objects.filter(id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
.first()
.id
]
comptable.groups.set(
[
Group.objects.filter(id=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
.first()
.id
]
)
comptable.save()
# Adding user Guy
u = User(
@ -357,11 +365,11 @@ Welcome to the wiki page!
PageRev(
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
).save()
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
p.save(force_lock=True)
p = Page(name="Services")
p.save(force_lock=True)
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
p.view_groups.set([settings.SITH_GROUP_PUBLIC_ID])
p.save(force_lock=True)
PageRev(
page=p,
@ -375,13 +383,6 @@ Welcome to the wiki page!
""",
).save()
# Adding README
p = Page(name="README")
p.save(force_lock=True)
p.view_groups = [settings.SITH_GROUP_PUBLIC_ID]
p.save(force_lock=True)
with open(os.path.join(root_path) + "/README.md", "r") as rm:
PageRev(page=p, title="README", author=skia, content=rm.read()).save()
# Subscription
default_subscription = "un-semestre"
@ -847,9 +848,9 @@ Welcome to the wiki page!
)
comunity.set_password("plop")
comunity.save()
comunity.groups = [
Group.objects.filter(name="Communication admin").first().id
]
comunity.groups.set(
[Group.objects.filter(name="Communication admin").first().id]
)
comunity.save()
Membership(
user=comunity,
@ -857,6 +858,18 @@ Welcome to the wiki page!
start_date=timezone.now(),
role=settings.SITH_CLUB_ROLES_ID["Board member"],
).save()
# Adding user tutu
tutu = User(
username="tutu",
last_name="Tu",
first_name="Tu",
email="tutu@git.an",
date_of_birth="1942-06-12",
)
tutu.set_password("plop")
tutu.save()
tutu.groups.set([settings.SITH_GROUP_PEDAGOGY_ADMIN_ID])
tutu.save()
# Adding subscription for sli
s = Subscription(
@ -895,6 +908,18 @@ Welcome to the wiki page!
start=s.subscription_start,
)
s.save()
# Tutu
s = Subscription(
member=tutu,
subscription_type=default_subscription,
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
)
s.subscription_start = s.compute_start()
s.subscription_end = s.compute_end(
duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]["duration"],
start=s.subscription_start,
)
s.save()
Selling(
label=dcons.name,
@ -1077,3 +1102,35 @@ Welcome to the wiki page!
start_date=friday + timedelta(hours=24 * 7 * i),
end_date=friday + timedelta(hours=24 * 7 * i + 8),
).save()
# Create som data for pedagogy
UV(
code="PA00",
author=User.objects.get(id=0),
credit_type=settings.SITH_PEDAGOGY_UV_TYPE[3][0],
manager="Laurent HEYBERGER",
semester=settings.SITH_PEDAGOGY_UV_SEMESTER[3][0],
language=settings.SITH_PEDAGOGY_UV_LANGUAGE[0][0],
department=settings.SITH_PROFILE_DEPARTMENTS[-2][0],
credits=5,
title="Participation dans une association étudiante",
objectives="* Permettre aux étudiants de réaliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.",
program="""* Semestre précédent proposition d'un projet et d'un cahier des charges
* Evaluation par un jury de six membres
* Si accord réalisation dans le cadre de l'UV
* Compte-rendu de l'expérience
* Présentation""",
skills="""* Gérer un projet associatif ou une action éducative en autonomie:
* en produisant un cahier des charges qui -définit clairement le contexte du projet personnel -pose les jalons de ce projet -estime de manière réaliste les moyens et objectifs du projet -définit exactement les livrables attendus
* en étant capable de respecter ce cahier des charges ou, le cas échéant, de réviser le cahier des charges de manière argumentée.
* Relater son expérience dans un rapport:
* qui permettra à d'autres étudiants de poursuivre les actions engagées
* qui montre la capacité à s'auto-évaluer et à adopter une distance critique sur son action.""",
key_concepts="""* Autonomie
* Responsabilité
* Cahier des charges
* Gestion de projet""",
hours_THE=121,
hours_TE=4,
).save()

View File

@ -25,7 +25,7 @@
import os
import re
from mistune import Renderer, InlineGrammar, InlineLexer, Markdown, escape, escape_link
from django.core.urlresolvers import reverse
from django.urls import reverse
class SithRenderer(Renderer):

View File

@ -37,8 +37,8 @@ AnonymousUser = getattr(importlib.import_module(module), klass)
def get_cached_user(request):
if not hasattr(request, "_cached_user"):
user = get_user(request)
if user.is_anonymous():
user = AnonymousUser(request)
if user.is_anonymous:
user = AnonymousUser()
request._cached_user = user

View File

@ -8,6 +8,7 @@ import django.core.validators
import core.models
import phonenumber_field.modelfields
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -276,6 +277,7 @@ class Migration(migrations.Migration):
(
"group_ptr",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
parent_link=True,
serialize=False,
@ -329,6 +331,7 @@ class Migration(migrations.Migration):
(
"owner_group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
default=1,
related_name="owned_page",
verbose_name="owner group",
@ -390,10 +393,19 @@ class Migration(migrations.Migration):
(
"author",
models.ForeignKey(
to=settings.AUTH_USER_MODEL, related_name="page_rev"
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
related_name="page_rev",
),
),
(
"page",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="core.Page",
related_name="revisions",
),
),
("page", models.ForeignKey(to="core.Page", related_name="revisions")),
],
options={"ordering": ["date"]},
),
@ -420,7 +432,9 @@ class Migration(migrations.Migration):
(
"user",
models.OneToOneField(
to=settings.AUTH_USER_MODEL, related_name="preferences"
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
related_name="preferences",
),
),
],
@ -469,6 +483,7 @@ class Migration(migrations.Migration):
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="owner",
to=settings.AUTH_USER_MODEL,
related_name="owned_files",
@ -477,6 +492,7 @@ class Migration(migrations.Migration):
(
"parent",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
null=True,
related_name="children",
verbose_name="parent",
@ -512,6 +528,7 @@ class Migration(migrations.Migration):
model_name="user",
name="home",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
blank=True,
null=True,
related_name="home_of",

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -21,6 +22,7 @@ class Migration(migrations.Migration):
model_name="page",
name="lock_user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="lock user",
default=None,
blank=True,

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.utils.timezone
import django.db.models.deletion
class Migration(migrations.Migration):
@ -48,7 +49,9 @@ class Migration(migrations.Migration):
(
"user",
models.ForeignKey(
related_name="notifications", to=settings.AUTH_USER_MODEL
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to=settings.AUTH_USER_MODEL,
),
),
],

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
model_name="sithfile",
name="moderator",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="moderated_files",
verbose_name="owner",
default=0,

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
model_name="sithfile",
name="moderator",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="moderated_files",
blank=True,
null=True,

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -35,7 +36,9 @@ class Migration(migrations.Migration):
model_name="preferences",
name="user",
field=models.OneToOneField(
related_name="_preferences", to=settings.AUTH_USER_MODEL
on_delete=django.db.models.deletion.CASCADE,
related_name="_preferences",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import django.utils.timezone
import django.db.models.deletion
class Migration(migrations.Migration):
@ -33,7 +34,9 @@ class Migration(migrations.Migration):
(
"user",
models.ForeignKey(
related_name="gifts", to=settings.AUTH_USER_MODEL
on_delete=django.db.models.deletion.CASCADE,
related_name="gifts",
to=settings.AUTH_USER_MODEL,
),
),
],

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
import core.models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -14,6 +15,7 @@ class Migration(migrations.Migration):
model_name="page",
name="owner_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="owner group",
default=core.models.Page.get_default_owner_group,
related_name="owned_page",

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-04 13:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0029_auto_20180426_2013")]
operations = [
migrations.AlterField(
model_name="notification",
name="type",
field=models.CharField(
choices=[
("POSTER_MODERATION", "A new poster needs to be moderated"),
("MAILING_MODERATION", "A new mailing list needs to be moderated"),
(
"PEDAGOGY_MODERATION",
"A new pedagogy comment has been signaled for moderation",
),
("NEWS_MODERATION", "There are %s fresh news to be moderated"),
("FILE_MODERATION", "New files to be moderated"),
(
"SAS_MODERATION",
"There are %s pictures to be moderated in the SAS",
),
("NEW_PICTURES", "You've been identified on some pictures"),
("REFILLING", "You just refilled of %s"),
("SELLING", "You just bought %s"),
("GENERIC", "You have a notification"),
],
default="GENERIC",
max_length=32,
verbose_name="type",
),
)
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-09-06 14:15
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0030_auto_20190704_1500")]
operations = [
migrations.AlterField(
model_name="sithfile",
name="is_folder",
field=models.BooleanField(
db_index=True, default=True, verbose_name="is folder"
),
),
migrations.AlterField(
model_name="sithfile",
name="is_in_sas",
field=models.BooleanField(
db_index=True, default=False, verbose_name="is in the SAS"
),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-09-08 22:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0031_auto_20190906_1615")]
operations = [
migrations.AlterField(
model_name="notification",
name="viewed",
field=models.BooleanField(
db_index=True, default=False, verbose_name="viewed"
),
)
]

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.6 on 2019-10-05 22:49
import django.contrib.auth.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("core", "0032_auto_20190909_0043")]
operations = [
migrations.AlterModelOptions(
name="page",
options={
"permissions": (
(
"change_prop_page",
"Can change the page's properties (groups, ...)",
),
)
},
),
migrations.AlterModelManagers(
name="group",
managers=[("objects", django.contrib.auth.models.GroupManager())],
),
]

View File

@ -38,7 +38,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core import validators
from django.core.exceptions import ValidationError, PermissionDenied
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.conf import settings
from django.db import transaction
from django.contrib.staticfiles.storage import staticfiles_storage
@ -659,7 +659,7 @@ class User(AbstractBaseUser):
class AnonymousUser(AuthAnonymousUser):
def __init__(self, request):
def __init__(self):
super(AnonymousUser, self).__init__()
@property
@ -670,6 +670,10 @@ class AnonymousUser(AuthAnonymousUser):
def was_subscribed(self):
return False
@property
def is_subscribed(self):
return False
@property
def subscribed(self):
return False
@ -739,7 +743,9 @@ class AnonymousUser(AuthAnonymousUser):
class Preferences(models.Model):
user = models.OneToOneField(User, related_name="_preferences")
user = models.OneToOneField(
User, related_name="_preferences", on_delete=models.CASCADE
)
receive_weekmail = models.BooleanField(
_("do you want to receive the weekmail"), default=False
)
@ -773,7 +779,12 @@ def get_thumbnail_directory(instance, filename):
class SithFile(models.Model):
name = models.CharField(_("file name"), max_length=256, blank=False)
parent = models.ForeignKey(
"self", related_name="children", verbose_name=_("parent"), null=True, blank=True
"self",
related_name="children",
verbose_name=_("parent"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
file = models.FileField(
upload_to=get_directory,
@ -796,14 +807,19 @@ class SithFile(models.Model):
null=True,
blank=True,
)
owner = models.ForeignKey(User, related_name="owned_files", verbose_name=_("owner"))
owner = models.ForeignKey(
User,
related_name="owned_files",
verbose_name=_("owner"),
on_delete=models.CASCADE,
)
edit_groups = models.ManyToManyField(
Group, related_name="editable_files", verbose_name=_("edit group"), blank=True
)
view_groups = models.ManyToManyField(
Group, related_name="viewable_files", verbose_name=_("view group"), blank=True
)
is_folder = models.BooleanField(_("is folder"), default=True)
is_folder = models.BooleanField(_("is folder"), default=True, db_index=True)
mime_type = models.CharField(_("mime type"), max_length=30)
size = models.IntegerField(_("size"), default=0)
date = models.DateTimeField(_("date"), default=timezone.now)
@ -814,10 +830,11 @@ class SithFile(models.Model):
verbose_name=_("owner"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
asked_for_removal = models.BooleanField(_("asked for removal"), default=False)
is_in_sas = models.BooleanField(
_("is in the SAS"), default=False
_("is in the SAS"), default=False, db_index=True
) # Allows to query this flag, updated at each call to save()
class Meta:
@ -931,8 +948,8 @@ class SithFile(models.Model):
def copy_rights(self):
"""Copy, if possible, the rights of the parent folder"""
if self.parent is not None:
self.edit_groups = self.parent.edit_groups.all()
self.view_groups = self.parent.view_groups.all()
self.edit_groups.set(self.parent.edit_groups.all())
self.view_groups.set(self.parent.view_groups.all())
self.save()
def move_to(self, parent):
@ -1129,6 +1146,7 @@ class Page(models.Model):
related_name="owned_page",
verbose_name=_("owner group"),
default=get_default_owner_group,
on_delete=models.CASCADE,
)
edit_groups = models.ManyToManyField(
Group, related_name="editable_page", verbose_name=_("edit group"), blank=True
@ -1143,6 +1161,7 @@ class Page(models.Model):
blank=True,
null=True,
default=None,
on_delete=models.CASCADE,
)
lock_timeout = models.DateTimeField(
_("lock_timeout"), null=True, blank=True, default=None
@ -1152,7 +1171,6 @@ class Page(models.Model):
unique_together = ("name", "parent")
permissions = (
("change_prop_page", "Can change the page's properties (groups, ...)"),
("view_page", "Can view the page"),
)
@staticmethod
@ -1343,8 +1361,8 @@ class PageRev(models.Model):
title = models.CharField(_("page title"), max_length=255, blank=True)
content = models.TextField(_("page content"), blank=True)
date = models.DateTimeField(_("date"), auto_now=True)
author = models.ForeignKey(User, related_name="page_rev")
page = models.ForeignKey(Page, related_name="revisions")
author = models.ForeignKey(User, related_name="page_rev", on_delete=models.CASCADE)
page = models.ForeignKey(Page, related_name="revisions", on_delete=models.CASCADE)
class Meta:
ordering = ["date"]
@ -1382,14 +1400,16 @@ class PageRev(models.Model):
class Notification(models.Model):
user = models.ForeignKey(User, related_name="notifications")
user = models.ForeignKey(
User, related_name="notifications", on_delete=models.CASCADE
)
url = models.CharField(_("url"), max_length=255)
param = models.CharField(_("param"), max_length=128, default="")
type = models.CharField(
_("type"), max_length=32, choices=settings.SITH_NOTIFICATIONS, default="GENERIC"
)
date = models.DateTimeField(_("date"), default=timezone.now)
viewed = models.BooleanField(_("viewed"), default=False)
viewed = models.BooleanField(_("viewed"), default=False, db_index=True)
def __str__(self):
if self.param:
@ -1418,7 +1438,7 @@ class Notification(models.Model):
class Gift(models.Model):
label = models.CharField(_("label"), max_length=255)
date = models.DateTimeField(_("date"), default=timezone.now)
user = models.ForeignKey(User, related_name="gifts")
user = models.ForeignKey(User, related_name="gifts", on_delete=models.CASCADE)
def __str__(self):
return "%s - %s" % (self.translated_label, self.date.strftime("%d %b %Y"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

4
core/static/core/easymde/easymde.min.css vendored Normal file → Executable file

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,782 +0,0 @@
/**
* @author zhixin wen <wenzhixin2010@gmail.com>
* @version 1.2.1
*
* http://wenzhixin.net.cn/p/multiple-select/
*/
(function ($) {
'use strict';
// it only does '%s', and return '' when arguments are undefined
var sprintf = function (str) {
var args = arguments,
flag = true,
i = 1;
str = str.replace(/%s/g, function () {
var arg = args[i++];
if (typeof arg === 'undefined') {
flag = false;
return '';
}
return arg;
});
return flag ? str : '';
};
var removeDiacritics = function (str) {
var defaultDiacriticsRemovalMap = [
{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
{'base':'AA','letters':/[\uA732]/g},
{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
{'base':'AO','letters':/[\uA734]/g},
{'base':'AU','letters':/[\uA736]/g},
{'base':'AV','letters':/[\uA738\uA73A]/g},
{'base':'AY','letters':/[\uA73C]/g},
{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
{'base':'DZ','letters':/[\u01F1\u01C4]/g},
{'base':'Dz','letters':/[\u01F2\u01C5]/g},
{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
{'base':'LJ','letters':/[\u01C7]/g},
{'base':'Lj','letters':/[\u01C8]/g},
{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
{'base':'NJ','letters':/[\u01CA]/g},
{'base':'Nj','letters':/[\u01CB]/g},
{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
{'base':'OI','letters':/[\u01A2]/g},
{'base':'OO','letters':/[\uA74E]/g},
{'base':'OU','letters':/[\u0222]/g},
{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
{'base':'TZ','letters':/[\uA728]/g},
{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
{'base':'VY','letters':/[\uA760]/g},
{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
{'base':'aa','letters':/[\uA733]/g},
{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
{'base':'ao','letters':/[\uA735]/g},
{'base':'au','letters':/[\uA737]/g},
{'base':'av','letters':/[\uA739\uA73B]/g},
{'base':'ay','letters':/[\uA73D]/g},
{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
{'base':'dz','letters':/[\u01F3\u01C6]/g},
{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
{'base':'hv','letters':/[\u0195]/g},
{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
{'base':'lj','letters':/[\u01C9]/g},
{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
{'base':'nj','letters':/[\u01CC]/g},
{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
{'base':'oi','letters':/[\u01A3]/g},
{'base':'ou','letters':/[\u0223]/g},
{'base':'oo','letters':/[\uA74F]/g},
{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
{'base':'tz','letters':/[\uA729]/g},
{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
{'base':'vy','letters':/[\uA761]/g},
{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
];
for (var i = 0; i < defaultDiacriticsRemovalMap.length; i++) {
str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base);
}
return str;
};
function MultipleSelect($el, options) {
var that = this,
name = $el.attr('name') || options.name || '';
this.options = options;
// hide select element
this.$el = $el.hide();
// label element
this.$label = this.$el.closest('label');
if (this.$label.length === 0 && this.$el.attr('id')) {
this.$label = $(sprintf('label[for="%s"]', this.$el.attr('id').replace(/:/g, '\\:')));
}
// restore class and title from select element
this.$parent = $(sprintf(
'<div class="ms-parent %s" %s/>',
$el.attr('class') || '',
sprintf('title="%s"', $el.attr('title'))));
// add placeholder to choice button
this.$choice = $(sprintf([
'<button type="button" class="ms-choice">',
'<span class="placeholder">%s</span>',
'<div></div>',
'</button>'
].join(''),
this.options.placeholder));
// default position is bottom
this.$drop = $(sprintf('<div class="ms-drop %s"%s></div>',
this.options.position,
sprintf(' style="width: %s"', this.options.dropWidth)));
this.$el.after(this.$parent);
this.$parent.append(this.$choice);
this.$parent.append(this.$drop);
if (this.$el.prop('disabled')) {
this.$choice.addClass('disabled');
}
this.$parent.css('width',
this.options.width ||
this.$el.css('width') ||
this.$el.outerWidth() + 20);
this.selectAllName = 'data-name="selectAll' + name + '"';
this.selectGroupName = 'data-name="selectGroup' + name + '"';
this.selectItemName = 'data-name="selectItem' + name + '"';
if (!this.options.keepOpen) {
$(document).click(function (e) {
if ($(e.target)[0] === that.$choice[0] ||
$(e.target).parents('.ms-choice')[0] === that.$choice[0]) {
return;
}
if (($(e.target)[0] === that.$drop[0] ||
$(e.target).parents('.ms-drop')[0] !== that.$drop[0] && e.target !== $el[0]) &&
that.options.isOpen) {
that.close();
}
});
}
}
MultipleSelect.prototype = {
constructor: MultipleSelect,
init: function () {
var that = this,
$ul = $('<ul></ul>');
this.$drop.html('');
if (this.options.filter) {
this.$drop.append([
'<div class="ms-search">',
'<input type="text" autocomplete="off" autocorrect="off" autocapitilize="off" spellcheck="false">',
'</div>'].join('')
);
}
if (this.options.selectAll && !this.options.single) {
$ul.append([
'<li class="ms-select-all">',
'<label>',
sprintf('<input type="checkbox" %s /> ', this.selectAllName),
this.options.selectAllDelimiter[0],
this.options.selectAllText,
this.options.selectAllDelimiter[1],
'</label>',
'</li>'
].join(''));
}
$.each(this.$el.children(), function (i, elm) {
$ul.append(that.optionToHtml(i, elm));
});
$ul.append(sprintf('<li class="ms-no-results">%s</li>', this.options.noMatchesFound));
this.$drop.append($ul);
this.$drop.find('ul').css('max-height', this.options.maxHeight + 'px');
this.$drop.find('.multiple').css('width', this.options.multipleWidth + 'px');
this.$searchInput = this.$drop.find('.ms-search input');
this.$selectAll = this.$drop.find('input[' + this.selectAllName + ']');
this.$selectGroups = this.$drop.find('input[' + this.selectGroupName + ']');
this.$selectItems = this.$drop.find('input[' + this.selectItemName + ']:enabled');
this.$disableItems = this.$drop.find('input[' + this.selectItemName + ']:disabled');
this.$noResults = this.$drop.find('.ms-no-results');
this.events();
this.updateSelectAll(true);
this.update(true);
if (this.options.isOpen) {
this.open();
}
},
optionToHtml: function (i, elm, group, groupDisabled) {
var that = this,
$elm = $(elm),
classes = $elm.attr('class') || '',
title = sprintf('title="%s"', $elm.attr('title')),
multiple = this.options.multiple ? 'multiple' : '',
disabled,
type = this.options.single ? 'radio' : 'checkbox';
if ($elm.is('option')) {
var value = $elm.val(),
text = that.options.textTemplate($elm),
selected = $elm.prop('selected'),
style = sprintf('style="%s"', this.options.styler(value)),
$el;
disabled = groupDisabled || $elm.prop('disabled');
$el = $([
sprintf('<li class="%s %s" %s %s>', multiple, classes, title, style),
sprintf('<label class="%s">', disabled ? 'disabled' : ''),
sprintf('<input type="%s" %s%s%s%s>',
type, this.selectItemName,
selected ? ' checked="checked"' : '',
disabled ? ' disabled="disabled"' : '',
sprintf(' data-group="%s"', group)),
sprintf('<span>%s</span>', text),
'</label>',
'</li>'
].join(''));
$el.find('input').val(value);
return $el;
}
if ($elm.is('optgroup')) {
var label = that.options.labelTemplate($elm),
$group = $('<div/>');
group = 'group_' + i;
disabled = $elm.prop('disabled');
$group.append([
'<li class="group">',
sprintf('<label class="optgroup %s" data-group="%s">', disabled ? 'disabled' : '', group),
this.options.hideOptgroupCheckboxes || this.options.single ? '' :
sprintf('<input type="checkbox" %s %s>',
this.selectGroupName, disabled ? 'disabled="disabled"' : ''),
label,
'</label>',
'</li>'
].join(''));
$.each($elm.children(), function (i, elm) {
$group.append(that.optionToHtml(i, elm, group, disabled));
});
return $group.html();
}
},
events: function () {
var that = this,
toggleOpen = function (e) {
e.preventDefault();
that[that.options.isOpen ? 'close' : 'open']();
};
if (this.$label) {
this.$label.off('click').on('click', function (e) {
if (e.target.nodeName.toLowerCase() !== 'label' || e.target !== this) {
return;
}
toggleOpen(e);
if (!that.options.filter || !that.options.isOpen) {
that.focus();
}
e.stopPropagation(); // Causes lost focus otherwise
});
}
this.$choice.off('click').on('click', toggleOpen)
.off('focus').on('focus', this.options.onFocus)
.off('blur').on('blur', this.options.onBlur);
this.$parent.off('keydown').on('keydown', function (e) {
switch (e.which) {
case 27: // esc key
that.close();
that.$choice.focus();
break;
}
});
this.$searchInput.off('keydown').on('keydown',function (e) {
// Ensure shift-tab causes lost focus from filter as with clicking away
if (e.keyCode === 9 && e.shiftKey) {
that.close();
}
}).off('keyup').on('keyup', function (e) {
// enter or space
// Avoid selecting/deselecting if no choices made
if (that.options.filterAcceptOnEnter && (e.which === 13 || e.which == 32) && that.$searchInput.val()) {
that.$selectAll.click();
that.close();
that.focus();
return;
}
that.filter();
});
this.$selectAll.off('click').on('click', function () {
var checked = $(this).prop('checked'),
$items = that.$selectItems.filter(':visible');
if ($items.length === that.$selectItems.length) {
that[checked ? 'checkAll' : 'uncheckAll']();
} else { // when the filter option is true
that.$selectGroups.prop('checked', checked);
$items.prop('checked', checked);
that.options[checked ? 'onCheckAll' : 'onUncheckAll']();
that.update();
}
});
this.$selectGroups.off('click').on('click', function () {
var group = $(this).parent().attr('data-group'),
$items = that.$selectItems.filter(':visible'),
$children = $items.filter(sprintf('[data-group="%s"]', group)),
checked = $children.length !== $children.filter(':checked').length;
$children.prop('checked', checked);
that.updateSelectAll();
that.update();
that.options.onOptgroupClick({
label: $(this).parent().text(),
checked: checked,
children: $children.get(),
instance: that
});
});
this.$selectItems.off('click').on('click', function () {
that.updateSelectAll();
that.update();
that.updateOptGroupSelect();
that.options.onClick({
label: $(this).parent().text(),
value: $(this).val(),
checked: $(this).prop('checked'),
instance: that
});
if (that.options.single && that.options.isOpen && !that.options.keepOpen) {
that.close();
}
if (that.options.single) {
var clickedVal = $(this).val();
that.$selectItems.filter(function() {
return $(this).val() !== clickedVal;
}).each(function() {
$(this).prop('checked', false);
});
that.update();
}
});
},
open: function () {
if (this.$choice.hasClass('disabled')) {
return;
}
this.options.isOpen = true;
this.$choice.find('>div').addClass('open');
this.$drop[this.animateMethod('show')]();
// fix filter bug: no results show
this.$selectAll.parent().show();
this.$noResults.hide();
// Fix #77: 'All selected' when no options
if (!this.$el.children().length) {
this.$selectAll.parent().hide();
this.$noResults.show();
}
if (this.options.container) {
var offset = this.$drop.offset();
this.$drop.appendTo($(this.options.container));
this.$drop.offset({
top: offset.top,
left: offset.left
});
}
if (this.options.filter) {
this.$searchInput.val('');
this.$searchInput.focus();
this.filter();
}
this.options.onOpen();
},
close: function () {
this.options.isOpen = false;
this.$choice.find('>div').removeClass('open');
this.$drop[this.animateMethod('hide')]();
if (this.options.container) {
this.$parent.append(this.$drop);
this.$drop.css({
'top': 'auto',
'left': 'auto'
});
}
this.options.onClose();
},
animateMethod: function (method) {
var methods = {
show: {
fade: 'fadeIn',
slide: 'slideDown'
},
hide: {
fade: 'fadeOut',
slide: 'slideUp'
}
};
return methods[method][this.options.animate] || method;
},
update: function (isInit) {
var selects = this.options.displayValues ? this.getSelects() : this.getSelects('text'),
$span = this.$choice.find('>span'),
sl = selects.length;
if (sl === 0) {
$span.addClass('placeholder').html(this.options.placeholder);
} else if (this.options.allSelected && sl === this.$selectItems.length + this.$disableItems.length) {
$span.removeClass('placeholder').html(this.options.allSelected);
} else if (this.options.ellipsis && sl > this.options.minimumCountSelected) {
$span.removeClass('placeholder').text(selects.slice(0, this.options.minimumCountSelected)
.join(this.options.delimiter) + '...');
} else if (this.options.countSelected && sl > this.options.minimumCountSelected) {
$span.removeClass('placeholder').html(this.options.countSelected
.replace('#', selects.length)
.replace('%', this.$selectItems.length + this.$disableItems.length));
} else {
$span.removeClass('placeholder').text(selects.join(this.options.delimiter));
}
if (this.options.addTitle) {
$span.prop('title', this.getSelects('text'));
}
// set selects to select
this.$el.val(this.getSelects()).trigger('change');
// add selected class to selected li
this.$drop.find('li').removeClass('selected');
this.$drop.find('input:checked').each(function () {
$(this).parents('li').first().addClass('selected');
});
// trigger <select> change event
if (!isInit) {
this.$el.trigger('change');
}
},
updateSelectAll: function (isInit) {
var $items = this.$selectItems;
if (!isInit) {
$items = $items.filter(':visible');
}
this.$selectAll.prop('checked', $items.length &&
$items.length === $items.filter(':checked').length);
if (!isInit && this.$selectAll.prop('checked')) {
this.options.onCheckAll();
}
},
updateOptGroupSelect: function () {
var $items = this.$selectItems.filter(':visible');
$.each(this.$selectGroups, function (i, val) {
var group = $(val).parent().attr('data-group'),
$children = $items.filter(sprintf('[data-group="%s"]', group));
$(val).prop('checked', $children.length &&
$children.length === $children.filter(':checked').length);
});
},
//value or text, default: 'value'
getSelects: function (type) {
var that = this,
texts = [],
values = [];
this.$drop.find(sprintf('input[%s]:checked', this.selectItemName)).each(function () {
texts.push($(this).parents('li').first().text());
values.push($(this).val());
});
if (type === 'text' && this.$selectGroups.length) {
texts = [];
this.$selectGroups.each(function () {
var html = [],
text = $.trim($(this).parent().text()),
group = $(this).parent().data('group'),
$children = that.$drop.find(sprintf('[%s][data-group="%s"]', that.selectItemName, group)),
$selected = $children.filter(':checked');
if (!$selected.length) {
return;
}
html.push('[');
html.push(text);
if ($children.length > $selected.length) {
var list = [];
$selected.each(function () {
list.push($(this).parent().text());
});
html.push(': ' + list.join(', '));
}
html.push(']');
texts.push(html.join(''));
});
}
return type === 'text' ? texts : values;
},
setSelects: function (values) {
var that = this;
this.$selectItems.prop('checked', false);
this.$disableItems.prop('checked', false);
$.each(values, function (i, value) {
that.$selectItems.filter(sprintf('[value="%s"]', value)).prop('checked', true);
that.$disableItems.filter(sprintf('[value="%s"]', value)).prop('checked', true);
});
this.$selectAll.prop('checked', this.$selectItems.length ===
this.$selectItems.filter(':checked').length + this.$disableItems.filter(':checked').length);
$.each(that.$selectGroups, function (i, val) {
var group = $(val).parent().attr('data-group'),
$children = that.$selectItems.filter('[data-group="' + group + '"]');
$(val).prop('checked', $children.length &&
$children.length === $children.filter(':checked').length);
});
this.update();
},
enable: function () {
this.$choice.removeClass('disabled');
},
disable: function () {
this.$choice.addClass('disabled');
},
checkAll: function () {
this.$selectItems.prop('checked', true);
this.$selectGroups.prop('checked', true);
this.$selectAll.prop('checked', true);
this.update();
this.options.onCheckAll();
},
uncheckAll: function () {
this.$selectItems.prop('checked', false);
this.$selectGroups.prop('checked', false);
this.$selectAll.prop('checked', false);
this.update();
this.options.onUncheckAll();
},
focus: function () {
this.$choice.focus();
this.options.onFocus();
},
blur: function () {
this.$choice.blur();
this.options.onBlur();
},
refresh: function () {
this.init();
},
filter: function () {
var that = this,
text = $.trim(this.$searchInput.val()).toLowerCase();
if (text.length === 0) {
this.$selectAll.parent().show();
this.$selectItems.parent().show();
this.$disableItems.parent().show();
this.$selectGroups.parent().show();
this.$noResults.hide();
} else {
this.$selectItems.each(function () {
var $parent = $(this).parent();
$parent[removeDiacritics($parent.text().toLowerCase()).indexOf(removeDiacritics(text)) < 0 ? 'hide' : 'show']();
});
this.$disableItems.parent().hide();
this.$selectGroups.each(function () {
var $parent = $(this).parent();
var group = $parent.attr('data-group'),
$items = that.$selectItems.filter(':visible');
$parent[$items.filter(sprintf('[data-group="%s"]', group)).length ? 'show' : 'hide']();
});
//Check if no matches found
if (this.$selectItems.parent().filter(':visible').length) {
this.$selectAll.parent().show();
this.$noResults.hide();
} else {
this.$selectAll.parent().hide();
this.$noResults.show();
}
}
this.updateOptGroupSelect();
this.updateSelectAll();
this.options.onFilter(text);
}
};
$.fn.multipleSelect = function () {
var option = arguments[0],
args = arguments,
value,
allowedMethods = [
'getSelects', 'setSelects',
'enable', 'disable',
'open', 'close',
'checkAll', 'uncheckAll',
'focus', 'blur',
'refresh', 'close'
];
this.each(function () {
var $this = $(this),
data = $this.data('multipleSelect'),
options = $.extend({}, $.fn.multipleSelect.defaults,
$this.data(), typeof option === 'object' && option);
if (!data) {
data = new MultipleSelect($this, options);
$this.data('multipleSelect', data);
}
if (typeof option === 'string') {
if ($.inArray(option, allowedMethods) < 0) {
throw 'Unknown method: ' + option;
}
value = data[option](args[1]);
} else {
data.init();
if (args[1]) {
value = data[args[1]].apply(data, [].slice.call(args, 2));
}
}
});
return typeof value !== 'undefined' ? value : this;
};
$.fn.multipleSelect.defaults = {
name: '',
isOpen: false,
placeholder: '',
selectAll: true,
selectAllDelimiter: ['[', ']'],
minimumCountSelected: 3,
ellipsis: false,
multiple: false,
multipleWidth: 80,
single: false,
filter: false,
width: undefined,
dropWidth: undefined,
maxHeight: 250,
container: null,
position: 'bottom',
keepOpen: false,
animate: 'none', // 'none', 'fade', 'slide'
displayValues: false,
delimiter: ', ',
addTitle: false,
filterAcceptOnEnter: false,
hideOptgroupCheckboxes: false,
selectAllText: 'Tout sélectionner',
allSelected: 'Tout sélectionné',
countSelected: '# sur % sélectionnés',
noMatchesFound: 'Introuvable',
styler: function () {
return false;
},
textTemplate: function ($elm) {
return $elm.html();
},
labelTemplate: function ($elm) {
return $elm.attr('label');
},
onOpen: function () {
return false;
},
onClose: function () {
return false;
},
onCheckAll: function () {
return false;
},
onUncheckAll: function () {
return false;
},
onFocus: function () {
return false;
},
onBlur: function () {
return false;
},
onOptgroupClick: function () {
return false;
},
onClick: function () {
return false;
},
onFilter: function () {
return false;
}
};
})(jQuery);

View File

@ -38,7 +38,21 @@ $( function() {
$("#quick_notif li").click(function () {
$(this).hide();
})
} );
});
function createQuickNotif(msg) {
const el = document.createElement('li')
el.textContent = msg
el.addEventListener('click', () => el.parentNode.removeChild(el))
document.getElementById('quick_notif').appendChild(el)
}
function deleteQuickNotifs() {
const el = document.getElementById('quick_notif')
while (el.firstChild) {
el.removeChild(el.firstChild)
}
}
function display_notif() {
$('#header_notif').toggle().parent().toggleClass("white");
@ -52,4 +66,4 @@ function display_notif() {
// https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true
function getCSRFToken() {
return $("[name=csrfmiddlewaretoken]").val();
}
}

View File

@ -1,191 +0,0 @@
/**
* @author zhixin wen <wenzhixin2010@gmail.com>
*/
.ms-parent {
display: inline-block;
position: relative;
vertical-align: middle;
}
.ms-choice {
display: block;
width: 100%;
height: 26px;
padding: 0;
overflow: hidden;
cursor: pointer;
border: 1px solid #aaa;
text-align: left;
white-space: nowrap;
line-height: 26px;
color: #444;
text-decoration: none;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
background-color: #fff;
}
.ms-choice.disabled {
background-color: #f4f4f4;
background-image: none;
border: 1px solid #ddd;
cursor: default;
}
.ms-choice > span {
position: absolute;
top: 0;
left: 0;
right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
padding-left: 8px;
}
.ms-choice > span.placeholder {
color: #999;
}
.ms-choice > div {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 25px;
background: url('multiple-select.png') left top no-repeat;
}
.ms-choice > div.open {
background: url('multiple-select.png') right top no-repeat;
}
.ms-drop {
width: 100%;
overflow: hidden;
display: none;
margin-top: -1px;
padding: 0;
position: absolute;
z-index: 1000;
background: #fff;
color: #000;
border: 1px solid #aaa;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.ms-drop.bottom {
top: 100%;
-webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
-moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
}
.ms-drop.top {
bottom: 100%;
-webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
-moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
}
.ms-search {
display: inline-block;
margin: 0;
min-height: 26px;
padding: 4px;
position: relative;
white-space: nowrap;
width: 100%;
z-index: 10000;
}
.ms-search input {
width: 100%;
height: auto !important;
min-height: 24px;
padding: 0 20px 0 5px;
margin: 0;
outline: 0;
font-family: sans-serif;
font-size: 1em;
border: 1px solid #aaa;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background: #fff url('multiple-select.png') no-repeat 100% -22px;
background: url('multiple-select.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
background: url('multiple-select.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
background: url('multiple-select.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
background: url('multiple-select.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
background: url('multiple-select.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
background: url('multiple-select.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
}
.ms-search, .ms-search input {
-webkit-box-sizing: border-box;
-khtml-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.ms-drop ul {
overflow: auto;
margin: 0;
padding: 5px 8px;
}
.ms-drop ul > li {
list-style: none;
display: list-item;
background-image: none;
position: static;
}
.ms-drop ul > li .disabled {
opacity: .35;
filter: Alpha(Opacity=35);
}
.ms-drop ul > li.multiple {
display: block;
float: left;
}
.ms-drop ul > li.group {
clear: both;
}
.ms-drop ul > li.multiple label {
width: 100%;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ms-drop ul > li label {
font-weight: normal;
display: block;
white-space: nowrap;
}
.ms-drop ul > li label.optgroup {
font-weight: bold;
}
.ms-drop input[type="checkbox"] {
vertical-align: middle;
}
.ms-drop .ms-no-results {
display: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -30,6 +30,12 @@ $shadow-color: rgb(223, 223, 223);
$background-bouton-color: hsl(0, 0%, 90%);
/*--------------------------MEDIA QUERY HELPERS------------------------*/
$small-devices: 576px;
$medium-devices: 768px;
$large-devices: 992px;
$extra-large-devices: 1200px;
/*--------------------------------GENERAL------------------------------*/
body {
@ -1564,7 +1570,6 @@ footer {
form {
margin: 0px auto;
margin-bottom: 10px;
width: 60%;
}
label {
@ -1668,3 +1673,512 @@ label {
}
}
/* --------------------------------------pedagogy-----------------------------------*/
$pedagogy-blue: #1bb9ea;
$pedagogy-orange: #ea7900;
$pedagogy-hover-blue: #0e97ce;
$pedagogy-light-blue: #caf0ff;
$pedagogy-white-text: #f0f0f0;
.pedagogy {
&.star-not-checked {
color : #f7f7f7;
margin-bottom: 0px;
margin-top: 0px;
}
&.star-checked {
color: $pedagogy-orange;
margin-bottom: 0px;
margin-top: 0px;
}
&.grade-without-star {
display: none
}
@media screen and (max-width: $large-devices){
&.star-not-checked {
margin-left: 5px;
margin-right: 5px;
}
&.star-checked {
margin-left: 5px;
margin-right: 5px;
}
}
@media screen and (max-width: $small-devices){
&.grade-without-star {
display: block;
}
&.grade-with-star {
display: none;
}
}
#dynamic_view {
font-size: 1.1em;
overflow-wrap: break-word;
td {
text-align: center;
border: none;
}
}
#search_form {
.search-form-container {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"action-bar action-bar"
"search-bar search-bar"
"radio-department radio-department"
"radio-credit-type radio-semester";
}
.action-bar {
grid-area: action-bar;
margin-bottom: 10px;
}
.search-bar {
grid-area: search-bar;
display: grid;
grid-template-columns: auto 200px;
grid-template-rows: auto;
grid-template-areas: "search-bar-input search-bar-button";
@media screen and (max-width: $medium-devices){
grid-template-columns: auto auto;
grid-template-rows: auto;
grid-template-areas: "search-bar-input search-bar-button";
}
@media screen and (max-width: $small-devices){
grid-template-columns: auto;
grid-template-rows: auto;
grid-template-areas: "search-bar-input";
.search-bar-button {
display: none;
}
}
.search-bar-input {
grid-area: search-bar-input;
background: $pedagogy-light-blue;
}
.search-bar-button {
grid-area: search-bar-button;
background: $pedagogy-orange;
color: white;
font-weight: bold;
margin-left: 20px;
}
}
.radio-department {
grid-area: radio-department;
}
.radio-credit-type {
grid-area: radio-credit-type;
}
.radio-semester {
grid-area: radio-semester;
}
.radio-guide input[type="radio"],input[type="checkbox"] {
display:none;
}
.radio-guide {
margin-top: 10px;
color: white;
}
.radio-guide label {
display: inline-block;
background-color: $pedagogy-blue;
padding: 10px 20px;
font-family: Arial, sans-serif;
font-size: 16px;
border-radius: 4px;
}
.radio-guide input[type="radio"]:checked + label {
background-color: $pedagogy-orange;
}
.radio-guide input[type="checkbox"]:checked + label {
background-color: $pedagogy-orange;
}
.radio-guide label:hover {
background-color: $pedagogy-hover-blue;
}
}
#uv_detail {
color: #062f38;
.uv-quick-info-container {
display: grid;
grid-template-columns: 20% 20% 20% 20% auto;
grid-template-rows: auto auto;
grid-template-areas:
"hours-cm hours-td hours-tp hours-te hours-the"
"department credit-type semester . ." ;
}
.department {
grid-area: department;
}
.credit-type {
grid-area: credit-type;
}
.semester {
grid-area: semester;
}
.hours-cm {
grid-area: hours-cm;
}
.hours-td {
grid-area: hours-td;
}
.hours-tp {
grid-area: hours-tp;
}
.hours-te {
grid-area: hours-te;
}
.hours-the {
grid-area: hours-the;
}
#leave_comment_not_allowed {
p {
text-align: center;
color: red;
}
}
#leave_comment {
.leave-comment-grid-container {
display: grid;
grid-template-columns: 270px auto;
grid-template-rows: 100%;
grid-template-areas: "stars comment";
@media screen and (max-width: $large-devices){
grid-template-columns: 100%;
grid-template-rows: auto auto;
grid-template-areas:
"stars"
"comment";
}
}
.ui-accordion-content {
background-color: $white-color;
border-color: $pedagogy-orange;
border-right: none;
}
.form-stars {
grid-area: stars;
}
.form-comment {
grid-area: comment;
}
.ui-accordion-header {
background-color: $pedagogy-orange;
color: $pedagogy-white-text;
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
@media screen and (max-width: $large-devices){
clip-path: none;
}
}
.ui-accordion-header-icon {
color: $pedagogy-white-text;
margin-right: 10px;
}
.input-stars {
margin-top: 20px;
}
input[type="submit"] {
float: right;
}
}
.uv-details-container {
display: grid;
grid-template-columns: 150px 100px auto;
grid-template-rows: 156px 1fr;
grid-template-areas:
"grade grade-stars uv-infos"
". . uv-infos";
@media screen and (max-width: $large-devices){
grid-template-columns: 50% 50%;
grid-template-rows: auto auto;
grid-template-areas:
"grade grade-stars"
"uv-infos uv-infos";
}
}
.grade {
grid-area: grade;
color: $pedagogy-white-text;
background-color: $pedagogy-blue;
padding-right: 10px;
> p {
text-align: right;
font-weight: bold;
}
}
.grade-stars {
grid-area: grade-stars;
color: $pedagogy-white-text;
background-color: $pedagogy-blue;
font-weight: bold;
}
.uv-infos {
grid-area: uv-infos;
padding-left: 10px;
}
.comment-container {
display: grid;
grid-template-columns: 300px auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"grade-block comment"
"grade-block info"
"comment-end-bar comment-end-bar";
margin-bottom: 30px;
margin-top: 10px;
@media screen and (max-width: $large-devices){
grid-template-columns: auto;
grid-template-rows: auto auto auto auto;
grid-template-areas:
"grade-block"
"comment"
"info"
"comment-end-bar"
}
.grade-block {
grid-area: grade-block;
width: 300px;
display: grid;
grid-template-columns: 150px 150px;
grid-template-rows: 156px auto;
grid-template-areas:
"grade-type grade-stars"
"grade-extension grade-extension";
grid-gap: 15px;
clip-path: polygon(0 0, 0 100%, 100% 100%, 100% 30px, 270px 0);
align-items: start;
background-color: $pedagogy-blue;
@media screen and (max-width: $large-devices){
grid-template-columns: 50% auto;
grid-template-rows: auto;
grid-template-areas:"grade-type grade-stars";
width: auto;
clip-path: none;
align-content: space-evenly;
align-items: end;
}
.grade-extension {
grid-area: grade-extension;
background-color: $pedagogy-blue;
}
.grade-type {
grid-area: grade-type;
> p {
color: $pedagogy-white-text;
font-weight: bold;
text-align: right;
}
}
.grade-stars {
grid-area: grade-stars;
}
}
.comment {
grid-area: comment;
display: grid;
grid-template-columns: auto;
grid-template-rows: auto auto;
grid-template-areas:
"anchor"
"markdown";
@media screen and (max-width: $large-devices){
border-left: solid;
border-right: solid;
border-color: $pedagogy-blue;
}
.anchor {
grid-area: anchor;
text-align: right;
margin-right: 15px;
}
.markdown {
grid-area: markdown;
min-height: 139px;
margin-top: 0px;
margin-right: 0px;
padding: 10px;
text-align: justify;
overflow: auto;
}
}
.info {
grid-area: info;
padding-bottom: 10px;
@media screen and (max-width: $large-devices){
border-left: solid;
border-right: solid;
border-color: $pedagogy-blue;
}
.status-reported {
color: red;
float: left;
padding-left: 10px;
}
.actions {
float: right;
}
}
.comment-end-bar {
grid-area: comment-end-bar;
display: grid;
grid-template-columns: 33% auto auto;
grid-template-rows: 2.5em;
grid-template-areas: "author date report";
background-color: $pedagogy-blue;
margin-top: -1px;
@media screen and (max-width: $large-devices){
grid-template-columns: auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"report"
"date"
"author";
margin-top: 0px;
text-align: center;
}
.author {
grid-area: author;
padding-top: 6px;
padding-left: 20px;
background-color: $pedagogy-orange;
clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
@media screen and (max-width: $large-devices){
clip-path: none;
padding: 0px;
padding-bottom: 7px;
}
a {
color: $pedagogy-white-text;
font-weight: bold;
}
a:hover {
color: $pedagogy-hover-blue;
}
}
.date {
grid-area: date;
color: $pedagogy-white-text;
@media screen and (max-width: $large-devices){
padding-bottom: 7px;
}
}
.report {
grid-area: report;
justify-self: right;
padding-right: 30px;
padding-left: 30px;
a {
color: $pedagogy-white-text;
}
a:hover {
color: $pedagogy-hover-blue;
}
@media screen and (max-width: $large-devices){
text-align: center;
justify-self: inherit;
padding-bottom: 7px;
background-color: $white-color;
border-left: solid;
border-right: solid;
border-color: $pedagogy-blue;
a {
color: $black-color;
}
}
}
}
}
}
}

View File

@ -5,7 +5,6 @@
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('core/multiple-select.css') }}">
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
@ -39,7 +38,7 @@
</div>
<header>
{% if not user.is_authenticated() %}
{% if not user.is_authenticated %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;">
<a href="{{ url('core:index') }}"></a>
</div>
@ -60,21 +59,23 @@
</div>
<div id="header_bar">
<ul id="header_bars_infos">
{% for bar in Counter.objects.filter(type="BAR").all() %}
<li>
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
{% if bar.is_inactive(): %}
<i class="fa fa-question" style="color: #f39c12"></i>
{% elif bar.is_open(): %}
<i class="fa fa-check" style="color: #2ecc71"></i>
{% else %}
<i class="fa fa-times" style="color: #eb2f06"></i>
{% endif %}
{{ bar }}
</a>
</li>
{% endfor %}
</ul>
{% cache 100 counters_activity %}
{% for bar in Counter.objects.filter(type="BAR").all() %}
<li>
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
{% if bar.is_inactive(): %}
<i class="fa fa-question" style="color: #f39c12"></i>
{% elif bar.is_open(): %}
<i class="fa fa-check" style="color: #2ecc71"></i>
{% else %}
<i class="fa fa-times" style="color: #eb2f06"></i>
{% endif %}
{{ bar }}
</a>
</li>
{% endfor %}
</ul>
{% endcache %}
<form action="{{ url('core:search') }}" method="GET" id="header_search">
<input type="text" placeholder="{% trans %}Search{% endtrans %}" name="query" id="search" />
<input type="submit" value="{% trans %}Search{% endtrans %}" style="display: none;" />
@ -152,7 +153,7 @@
<nav>
<a href="{{ url('core:index') }}">{% trans %}Main{% endtrans %}</a>
<div class="dropdown">
<button class="dropbtn">{% trans %}Associations & Clubs{% endtrans %}
<button class="dropbtn">{% trans %}Associations & Clubs{% endtrans %}
<i class="fa fa-caret-down"></i>
</button>
<div class="dropdown-content">
@ -164,30 +165,30 @@
<a href="{{ url('core:page', page_name='clubs/doceo') }}">{% trans %}Doceo{% endtrans %}</a>
<a href="{{ url('core:page', page_name='positions') }}">{% trans %}Positions{% endtrans %}</a>
</div>
</div>
</div>
<div class="dropdown">
<button class="dropbtn">{% trans %}Events{% endtrans %}
<button class="dropbtn">{% trans %}Events{% endtrans %}
<i class="fa fa-caret-down"></i>
</button>
<div class="dropdown-content">
<a href="{{ url('core:page', page_name='Index/calendrier_evenements') }}">{% trans %}Calendar{% endtrans %}</a>
<a href="{{ url('core:page', page_name='ga') }}">{% trans %}Big event{% endtrans %}</a>
</div>
</div>
</div>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
<a href="{{ url('sas:main') }}">{% trans %}Gallery{% endtrans %}</a>
<a href="{{ url('eboutic:main') }}">{% trans %}Eboutic{% endtrans %}</a>
<div class="dropdown">
<button class="dropbtn">{% trans %}Services{% endtrans %}
<button class="dropbtn">{% trans %}Services{% endtrans %}
<i class="fa fa-caret-down"></i>
</button>
<div class="dropdown-content">
<a href="{{ url('matmat:search_clear') }}">{% trans %}Matmatronch{% endtrans %}</a>
<a href="/launderette">{% trans %}Launderette{% endtrans %}</a>
<a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a>
{# <a href="https://ae2.utbm.fr/uvs/">{% trans %}Pedagogy{% endtrans %}</a> #}
<a href="{{ url('pedagogy:guide') }}">{% trans %}Pedagogy{% endtrans %}</a>
</div>
</div>
</div>
<div class="dropdown">
<button class="dropbtn">{% trans %}My Benefits{% endtrans %}
<i class="fa fa-caret-down"></i>
@ -198,7 +199,7 @@
</div>
</div>
<div class="dropdown">
<button class="dropbtn">{% trans %}Help{% endtrans %}
<button class="dropbtn">{% trans %}Help{% endtrans %}
<i class="fa fa-caret-down"></i>
</button>
<div class="dropdown-content">
@ -206,7 +207,7 @@
<a href="{{ url('core:page', 'contacts') }}">{% trans %}Contacts{% endtrans %}</a>
<a href="{{ url('core:page', page_name="Index") }}">{% trans %}Wiki{% endtrans %}</a>
</div>
</div>
</div>
</nav>
{% endif %}
{% endblock %}
@ -264,22 +265,9 @@
<script src="{{ static('core/js/ui/jquery-ui.min.js') }}"></script>
<script src="{{ static('core/js/ui/i18n/datepicker-fr.js') }}"></script>
<script src="{{ static('core/js/jquery.datetimepicker.full.min.js') }}"></script>
<script src="{{ static('core/js/multiple-select.js') }}"></script>
<script src="{{ static('ajax_select/js/ajax_select.js') }}"></script>
<script src="{{ url('javascript-catalog') }}"></script>
<script>
$('.select_single').multipleSelect({
single: true,
{% if not popup %}
position: 'top',
{% endif %}
});
$('.select_multiple').multipleSelect({
filter: true,
{% if not popup %}
position: 'top',
{% endif %}
});
$('.select_date').datepicker({
changeMonth: true,
changeYear: true,

View File

@ -1,9 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Welcome!{% endtrans %}
{% endblock %}
{% block content %}
{{ get_sith().index_page|markdown }}
{% endblock %}

View File

@ -11,11 +11,11 @@
{% endif %}
{% if next %}
{% if user.is_authenticated() %}
{% if user.is_authenticated %}
<p>{% trans %}Your account doesn't have access to this page. To proceed,
please login with an account that has access.{% endtrans %}</p>
{% else %}
<p>{% trans %}Please login to see this page.{% endtrans %}</p>
<p>{% trans %}Please login or create an account to see this page.{% endtrans %}</p>
{% endif %}
{% endif %}
@ -30,5 +30,6 @@
{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{{ url('core:password_reset') }}">{% trans %}Lost password?{% endtrans %}</a></p>
<p><a href="{{ url('core:register') }}">{% trans %}Create account{% endtrans %}</a></p>
{% endblock %}

View File

@ -114,22 +114,24 @@
{% endmacro %}
{% macro paginate(page_obj, paginator) %}
{% if page_obj.has_previous() or page_obj.has_next() %}
{% if page_obj.has_previous() %}
<a href="?page={{ page_obj.previous_page_number() }}">{% trans %}Previous{% endtrans %}</a>
{% else %}
<span class="disabled">{% trans %}Previous{% endtrans %}</span>
{% endif %}
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
{% for i in paginator.page_range %}
{% if page_obj.number == i %}
<span class="active">{{ i }} <span class="sr-only">({% trans %}current{% endtrans %})</span></span>
{% else %}
<a href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next() %}
<a href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
{% else %}
<a href="?page={{ i }}">{{ i }}</a>
<span class="disabled">{% trans %}Next{% endtrans %}</span>
{% endif %}
{% endfor %}
{% if page_obj.has_next() %}
<a href="?page={{ page_obj.next_page_number() }}">{% trans %}Next{% endtrans %}</a>
{% else %}
<span class="disabled">{% trans %}Next{% endtrans %}</span>
{% endif %}
{% endmacro %}

View File

@ -5,168 +5,199 @@
{# The easymde script can be included twice, it's safe in the code #}
<script src="{{ statics.js }}"> </script>
<script type="text/javascript">
var css = "{{ statics.css }}";
var lastAPICall;
$(function() {
const css = "{{ statics.css }}";
let lastAPICall;
// Only import the css once
if (!document.head.innerHTML.includes(css)){
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
}
// Only import the css once
if (!document.head.innerHTML.includes(css)) {
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
}
// Custom markdown parser
function customMarkdownParser(plainText, preview) {
$.ajax({
url: "{{ markdown_api_url }}",
method: "POST",
data: { text: plainText, csrfmiddlewaretoken: getCSRFToken() },
}).done(function (msg) {
preview.innerHTML = msg;
// Custom markdown parser
function customMarkdownParser(plainText, cb) {
$.ajax({
url: "{{ markdown_api_url }}",
method: "POST",
data: { text: plainText, csrfmiddlewaretoken: getCSRFToken() },
}).done(cb);
}
// Pretty markdown input
const easymde = new EasyMDE({
element: document.getElementById("{{ widget.attrs.id }}"),
spellChecker: false,
autoDownloadFontAwesome: false,
previewRender: function(plainText, preview) { // Async method
clearTimeout(lastAPICall);
lastAPICall = setTimeout(() => {
customMarkdownParser(plainText, (msg) => preview.innerHTML = msg);
}, 300);
return preview.innerHTML;
},
forceSync: true, // Avoid validation error on generic create view
toolbar: [
{
name: "heading-smaller",
action: EasyMDE.toggleHeadingSmaller,
className: "fa fa-header",
title: "{{ translations.heading_smaller }}"
},
{
name: "italic",
action: EasyMDE.toggleItalic,
className: "fa fa-italic",
title: "{{ translations.italic }}"
},
{
name: "bold",
action: EasyMDE.toggleBold,
className: "fa fa-bold",
title: "{{ translations.bold }}"
},
{
name: "strikethrough",
action: EasyMDE.toggleStrikethrough,
className: "fa fa-strikethrough",
title: "{{ translations.strikethrough }}"
},
{
name: "underline",
action: function customFunction(editor){
let cm = editor.codemirror;
cm.replaceSelection('__' + cm.getSelection() + '__');
},
className: "fa fa-underline",
title: "{{ translations.underline }}"
},
{
name: "superscript",
action: function customFunction(editor){
let cm = editor.codemirror;
cm.replaceSelection('<sup>' + cm.getSelection() + '</sup>');
},
className: "fa fa-superscript",
title: "{{ translations.superscript }}"
},
{
name: "subscript",
action: function customFunction(editor){
let cm = editor.codemirror;
cm.replaceSelection('<sub>' + cm.getSelection() + '</sub>');
},
className: "fa fa-subscript",
title: "{{ translations.subscript }}"
},
{
name: "code",
action: EasyMDE.toggleCodeBlock,
className: "fa fa-code",
title: "{{ translations.code }}"
},
"|",
{
name: "quote",
action: EasyMDE.toggleBlockquote,
className: "fa fa-quote-left",
title: "{{ translations.quote }}"
},
{
name: "unordered-list",
action: EasyMDE.toggleUnorderedList,
className: "fa fa-list-ul",
title: "{{ translations.unordered_list }}"
},
{
name: "ordered-list",
action: EasyMDE.toggleOrderedList,
className: "fa fa-list-ol",
title: "{{ translations.ordered_list }}"
},
"|",
{
name: "link",
action: EasyMDE.drawLink,
className: "fa fa-link",
title: "{{ translations.link }}"
},
{
name: "image",
action: EasyMDE.drawImage,
className: "fa fa-picture-o",
title: "{{ translations.image }}"
},
{
name: "table",
action: EasyMDE.drawTable,
className: "fa fa-table",
title: "{{ translations.table }}"
},
"|",
{
name: "clean-block",
action: EasyMDE.cleanBlock,
className: "fa fa-eraser fa-clean-block",
title: "{{ translations.clean_block }}"
},
"|",
{
name: "preview",
action: EasyMDE.togglePreview,
className: "fa fa-eye no-disable",
title: "{{ translations.preview }}"
},
{
name: "side-by-side",
action: EasyMDE.toggleSideBySide,
className: "fa fa-columns no-disable no-mobile",
title: "{{ translations.side_by_side }}"
},
{
name: "fullscreen",
action: EasyMDE.toggleFullScreen,
className: "fa fa-arrows-alt no-disable no-mobile",
title: "{{ translations.fullscreen }}"
},
"|",
{
name: "guide",
action: "/page/Aide_sur_la_syntaxe",
className: "fa fa-question-circle",
title: "{{ translations.guide }}"
},
]
});
}
// Pretty markdown input
var easymde = new EasyMDE({
element: document.getElementById("{{ widget.attrs.id }}"),
spellChecker: false,
autoDownloadFontAwesome: false,
previewRender: function(plainText, preview){ // Async method
clearTimeout(lastAPICall);
lastAPICall = setTimeout(function (plainText, preview){
customMarkdownParser(plainText, preview);
}, 300, plainText, preview);
return preview.innerHTML;
},
forceSync: true, // Avoid validation error on generic create view
toolbar: [
{
name: "heading-smaller",
action: EasyMDE.toggleHeadingSmaller,
className: "fa fa-header",
title: "{{ translations.heading_smaller }}"
},
{
name: "italic",
action: EasyMDE.toggleItalic,
className: "fa fa-italic",
title: "{{ translations.italic }}"
},
{
name: "bold",
action: EasyMDE.toggleBold,
className: "fa fa-bold",
title: "{{ translations.bold }}"
},
{
name: "strikethrough",
action: EasyMDE.toggleStrikethrough,
className: "fa fa-strikethrough",
title: "{{ translations.strikethrough }}"
},
{
name: "underline",
action: function customFunction(editor){
var cm = editor.codemirror;
cm.replaceSelection('__' + cm.getSelection() + '__');
},
className: "fa fa-underline",
title: "{{ translations.underline }}"
},
{
name: "superscript",
action: function customFunction(editor){
var cm = editor.codemirror;
cm.replaceSelection('<sup>' + cm.getSelection() + '</sup>');
},
className: "fa fa-superscript",
title: "{{ translations.superscript }}"
},
{
name: "subscript",
action: function customFunction(editor){
var cm = editor.codemirror;
cm.replaceSelection('<sub>' + cm.getSelection() + '</sub>');
},
className: "fa fa-subscript",
title: "{{ translations.subscript }}"
},
{
name: "code",
action: EasyMDE.toggleCodeBlock,
className: "fa fa-code",
title: "{{ translations.code }}"
},
"|",
{
name: "quote",
action: EasyMDE.toggleBlockquote,
className: "fa fa-quote-left",
title: "{{ translations.quote }}"
},
{
name: "unordered-list",
action: EasyMDE.toggleUnorderedList,
className: "fa fa-list-ul",
title: "{{ translations.unordered_list }}"
},
{
name: "ordered-list",
action: EasyMDE.toggleOrderedList,
className: "fa fa-list-ol",
title: "{{ translations.ordered_list }}"
},
"|",
{
name: "link",
action: EasyMDE.drawLink,
className: "fa fa-link",
title: "{{ translations.link }}"
},
{
name: "image",
action: EasyMDE.drawImage,
className: "fa fa-picture-o",
title: "{{ translations.image }}"
},
{
name: "table",
action: EasyMDE.drawTable,
className: "fa fa-table",
title: "{{ translations.table }}"
},
"|",
{
name: "clean-block",
action: EasyMDE.cleanBlock,
className: "fa fa-eraser fa-clean-block",
title: "{{ translations.clean_block }}"
},
"|",
{
name: "preview",
action: EasyMDE.togglePreview,
className: "fa fa-eye no-disable",
title: "{{ translations.preview }}"
},
{
name: "side-by-side",
action: EasyMDE.toggleSideBySide,
className: "fa fa-columns no-disable no-mobile",
title: "{{ translations.side_by_side }}"
},
{
name: "fullscreen",
action: EasyMDE.toggleFullScreen,
className: "fa fa-arrows-alt no-disable no-mobile",
title: "{{ translations.fullscreen }}"
},
"|",
{
name: "guide",
action: "/page/Aide_sur_la_syntaxe",
className: "fa fa-question-circle",
title: "{{ translations.guide }}"
},
]
});
const textarea = document.getElementById('{{ widget.attrs.id }}');
const submits = textarea
.closest('form')
.querySelectorAll('input[type="submit"]');
const parentDiv = textarea.parentElement;
let submitPressed = false;
function checkMarkdownInput(e) {
// an attribute is null if it does not exist, else a string
const required = textarea.getAttribute('required') != null;
const length = textarea.value.trim().length;
if (required && length == 0) {
parentDiv.style.boxShadow = 'red 0px 0px 1.5px 1px';
} else {
parentDiv.style.boxShadow = '';
}
}
function onSubmitClick(e) {
if (!submitPressed) {
easymde.codemirror.on('change', checkMarkdownInput);
}
submitPressed = true;
checkMarkdownInput(e);
}
submits.forEach((submit) => {
submit.addEventListener('click', onSubmitClick);
})
})
</script>
</div>
</div>

View File

@ -1,10 +1,14 @@
{% extends "core/base.jinja" %}
{% block content %}
{% if form %}
<form method="post" action="">
{% csrf_token %}
{{ form.as_p() }}
<input type="submit" value="{% trans %}Reset{% endtrans %}" />
</form>
{% else %}
{% trans %}It seems that this link has expired. To generate a new link, you can follow this link: {% endtrans %}<a href="{{ url('core:password_change') }}">{% trans %}lost password{% endtrans %}</a>.
{% endif %}
{% endblock %}

View File

@ -200,7 +200,7 @@ $( function() {
keys.push(e.keyCode);
if (keys.toString()==pattern) {
keys = [];
$("#right_column img").attr("src", "{{ static('core/img/yug.jpg') }}");
$("#user_profile_pictures_bigone img").attr("src", "{{ static('core/img/yug.jpg') }}");
}
if (keys.length==6) {
keys.shift();

View File

@ -83,7 +83,6 @@
<li><a href="{{ url('com:weekmail_destinations') }}">{% trans %}Weekmail destinations{% endtrans %}</a></li>
<li><a href="{{ url('com:news_new') }}">{% trans %}Create news{% endtrans %}</a></li>
<li><a href="{{ url('com:news_admin_list') }}">{% trans %}Moderate news{% endtrans %}</a></li>
<li><a href="{{ url('com:index_edit') }}">{% trans %}Edit index page{% endtrans %}</a></li>
<li><a href="{{ url('com:alert_edit') }}">{% trans %}Edit alert message{% endtrans %}</a></li>
<li><a href="{{ url('com:info_edit') }}">{% trans %}Edit information message{% endtrans %}</a></li>
<li><a href="{{ url('core:file_moderation') }}">{% trans %}Moderate files{% endtrans %}</a></li>
@ -104,6 +103,16 @@
<li><a href="{{ url('club:tools', club_id=m.club.id) }}">{{ m.club }}</a></li>
{% endfor %}
</ul>
<hr>
<h4>{% trans %}Pedagogy{% endtrans %}</h4>
<ul>
{% if user.is_in_group(settings.SITH_GROUP_PEDAGOGY_ADMIN_ID) or user.is_root %}
<li><a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a></li>
<li><a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a></li>
{% endif %}
</ul>
<hr>
<h4>{% trans %}Elections{% endtrans %}</h4>
<ul>
@ -113,6 +122,8 @@
<li><a href="{{ url('election:create') }}">{% trans %}Create a new election{% endtrans %}</a></li>
{%- endif -%}
</ul>
<hr>
<h4>{% trans %}Other tools{% endtrans %}</h4>
<ul>
<li><a href="{{ url('core:to_markdown') }}">{% trans %}Convert dokuwiki/BBcode syntax to Markdown{% endtrans %}</a></li>

View File

@ -25,7 +25,7 @@
import os
from django.test import Client, TestCase
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.management import call_command
from core.models import User, Group, Page
@ -396,6 +396,40 @@ http://git.an
)
class UserToolsTest(TestCase):
def setUp(self):
call_command("populate")
def test_anonymous_user_unauthorized(self):
response = self.client.get(reverse("core:user_tools"))
self.assertEquals(response.status_code, 403)
def test_page_is_working(self):
# Test for simple user
self.client.login(username="guy", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
# Test for root
self.client.login(username="root", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
# Test for skia
self.client.login(username="skia", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
# Test for comunity
self.client.login(username="comunity", password="plop")
response = self.client.get(reverse("core:user_tools"))
self.assertNotEquals(response.status_code, 500)
self.assertEquals(response.status_code, 200)
# TODO: many tests on the pages:
# - renaming a page
# - changing a page's parent --> check that page's children's full_name

View File

@ -23,189 +23,209 @@
#
#
from django.conf.urls import url
from django.urls import re_path
from core.views import *
urlpatterns = [
url(r"^$", index, name="index"),
url(r"^to_markdown$", ToMarkdownView.as_view(), name="to_markdown"),
url(r"^notifications$", NotificationList.as_view(), name="notification_list"),
url(r"^notification/(?P<notif_id>[0-9]+)$", notification, name="notification"),
re_path(r"^$", index, name="index"),
re_path(r"^to_markdown$", ToMarkdownView.as_view(), name="to_markdown"),
re_path(r"^notifications$", NotificationList.as_view(), name="notification_list"),
re_path(r"^notification/(?P<notif_id>[0-9]+)$", notification, name="notification"),
# Search
url(r"^search/$", search_view, name="search"),
url(r"^search_json/$", search_json, name="search_json"),
url(r"^search_user/$", search_user_json, name="search_user"),
re_path(r"^search/$", search_view, name="search"),
re_path(r"^search_json/$", search_json, name="search_json"),
re_path(r"^search_user/$", search_user_json, name="search_user"),
# Login and co
url(r"^login/$", login, name="login"),
url(r"^logout/$", logout, name="logout"),
url(r"^password_change/$", password_change, name="password_change"),
url(
re_path(r"^login/$", SithLoginView.as_view(), name="login"),
re_path(r"^logout/$", logout, name="logout"),
re_path(
r"^password_change/$", SithPasswordChangeView.as_view(), name="password_change"
),
re_path(
r"^password_change/(?P<user_id>[0-9]+)$",
password_root_change,
name="password_root_change",
),
url(r"^password_change/done$", password_change_done, name="password_change_done"),
url(r"^password_reset/$", password_reset, name="password_reset"),
url(r"^password_reset/done$", password_reset_done, name="password_reset_done"),
url(
re_path(
r"^password_change/done$",
SithPasswordChangeDoneView.as_view(),
name="password_change_done",
),
re_path(
r"^password_reset/$", SithPasswordResetView.as_view(), name="password_reset"
),
re_path(
r"^password_reset/done$",
SithPasswordResetDoneView.as_view(),
name="password_reset_done",
),
re_path(
r"^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
password_reset_confirm,
SithPasswordResetConfirmView.as_view(),
name="password_reset_confirm",
),
url(r"^reset/done/$", password_reset_complete, name="password_reset_complete"),
url(r"^register$", register, name="register"),
re_path(
r"^reset/done/$",
SithPasswordResetCompleteView.as_view(),
name="password_reset_complete",
),
re_path(r"^register$", register, name="register"),
# Group handling
url(r"^group/$", GroupListView.as_view(), name="group_list"),
url(r"^group/new$", GroupCreateView.as_view(), name="group_new"),
url(r"^group/(?P<group_id>[0-9]+)/$", GroupEditView.as_view(), name="group_edit"),
url(
re_path(r"^group/$", GroupListView.as_view(), name="group_list"),
re_path(r"^group/new$", GroupCreateView.as_view(), name="group_new"),
re_path(
r"^group/(?P<group_id>[0-9]+)/$", GroupEditView.as_view(), name="group_edit"
),
re_path(
r"^group/(?P<group_id>[0-9]+)/delete$",
GroupDeleteView.as_view(),
name="group_delete",
),
url(
re_path(
r"^group/(?P<group_id>[0-9]+)/detail$",
GroupTemplateView.as_view(),
name="group_detail",
),
# User views
url(r"^user/$", UserListView.as_view(), name="user_list"),
url(
re_path(r"^user/$", UserListView.as_view(), name="user_list"),
re_path(
r"^user/(?P<user_id>[0-9]+)/mini$",
UserMiniView.as_view(),
name="user_profile_mini",
),
url(r"^user/(?P<user_id>[0-9]+)/$", UserView.as_view(), name="user_profile"),
url(
re_path(r"^user/(?P<user_id>[0-9]+)/$", UserView.as_view(), name="user_profile"),
re_path(
r"^user/(?P<user_id>[0-9]+)/pictures$",
UserPicturesView.as_view(),
name="user_pictures",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/godfathers$",
UserGodfathersView.as_view(),
name="user_godfathers",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/godfathers/tree$",
UserGodfathersTreeView.as_view(),
name="user_godfathers_tree",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/godfathers/tree/pict$",
UserGodfathersTreePictureView.as_view(),
name="user_godfathers_tree_pict",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/godfathers/(?P<godfather_id>[0-9]+)/(?P<is_father>(True)|(False))/delete$",
DeleteUserGodfathers,
name="user_godfathers_delete",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/edit$",
UserUpdateProfileView.as_view(),
name="user_edit",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/profile_upload$",
UserUploadProfilePictView.as_view(),
name="user_profile_upload",
),
url(r"^user/(?P<user_id>[0-9]+)/clubs$", UserClubView.as_view(), name="user_clubs"),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/clubs$", UserClubView.as_view(), name="user_clubs"
),
re_path(
r"^user/(?P<user_id>[0-9]+)/prefs$",
UserPreferencesView.as_view(),
name="user_prefs",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/groups$",
UserUpdateGroupView.as_view(),
name="user_groups",
),
url(r"^user/tools/$", UserToolsView.as_view(), name="user_tools"),
url(
re_path(r"^user/tools/$", UserToolsView.as_view(), name="user_tools"),
re_path(
r"^user/(?P<user_id>[0-9]+)/account$",
UserAccountView.as_view(),
name="user_account",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/account/(?P<year>[0-9]+)/(?P<month>[0-9]+)$",
UserAccountDetailView.as_view(),
name="user_account_detail",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/stats$", UserStatsView.as_view(), name="user_stats"
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/gift/create$",
GiftCreateView.as_view(),
name="user_gift_create",
),
url(
re_path(
r"^user/(?P<user_id>[0-9]+)/gift/delete/(?P<gift_id>[0-9]+)/$",
GiftDeleteView.as_view(),
name="user_gift_delete",
),
# File views
# url(r'^file/add/(?P<popup>popup)?$', FileCreateView.as_view(), name='file_new'),
url(r"^file/(?P<popup>popup)?$", FileListView.as_view(), name="file_list"),
url(
# re_path(r'^file/add/(?P<popup>popup)?$', FileCreateView.as_view(), name='file_new'),
re_path(r"^file/(?P<popup>popup)?$", FileListView.as_view(), name="file_list"),
re_path(
r"^file/(?P<file_id>[0-9]+)/(?P<popup>popup)?$",
FileView.as_view(),
name="file_detail",
),
url(
re_path(
r"^file/(?P<file_id>[0-9]+)/edit/(?P<popup>popup)?$",
FileEditView.as_view(),
name="file_edit",
),
url(
re_path(
r"^file/(?P<file_id>[0-9]+)/prop/(?P<popup>popup)?$",
FileEditPropView.as_view(),
name="file_prop",
),
url(
re_path(
r"^file/(?P<file_id>[0-9]+)/delete/(?P<popup>popup)?$",
FileDeleteView.as_view(),
name="file_delete",
),
url(r"^file/moderation$", FileModerationView.as_view(), name="file_moderation"),
url(
re_path(r"^file/moderation$", FileModerationView.as_view(), name="file_moderation"),
re_path(
r"^file/(?P<file_id>[0-9]+)/moderate$",
FileModerateView.as_view(),
name="file_moderate",
),
url(r"^file/(?P<file_id>[0-9]+)/download$", send_file, name="download"),
re_path(r"^file/(?P<file_id>[0-9]+)/download$", send_file, name="download"),
# Page views
url(r"^page/$", PageListView.as_view(), name="page_list"),
url(r"^page/create$", PageCreateView.as_view(), name="page_new"),
url(
re_path(r"^page/$", PageListView.as_view(), name="page_list"),
re_path(r"^page/create$", PageCreateView.as_view(), name="page_new"),
re_path(
r"^page/(?P<page_id>[0-9]*)/delete$",
PageDeleteView.as_view(),
name="page_delete",
),
url(
re_path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/edit$",
PageEditView.as_view(),
name="page_edit",
),
url(
re_path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/prop$",
PagePropView.as_view(),
name="page_prop",
),
url(
re_path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/hist$",
PageHistView.as_view(),
name="page_hist",
),
url(
re_path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/rev/(?P<rev>[0-9]+)/",
PageRevView.as_view(),
name="page_rev",
),
url(
re_path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/$",
PageView.as_view(),
name="page",

View File

@ -189,9 +189,7 @@ def doku_to_markdown(text):
except:
new_text.append("> " * quote_level)
line = line.replace(quote.group(0), "")
final_quote_level = (
quote_level
) # Store quote_level to use at the end, since it will be modified during quit iteration
final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration
final_newline = False
for quote in quit: # Quit quotes (support multiple at a time)
line = line.replace(quote.group(0), "")
@ -240,9 +238,7 @@ def bbcode_to_markdown(text):
except:
new_text.append("> " * quote_level)
line = line.replace(quote.group(0), "")
final_quote_level = (
quote_level
) # Store quote_level to use at the end, since it will be modified during quit iteration
final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration
final_newline = False
for quote in quit: # Quit quotes (support multiple at a time)
line = line.replace(quote.group(0), "")

View File

@ -48,7 +48,7 @@ from core.models import Group
from core.views.forms import LoginForm
def forbidden(request):
def forbidden(request, exception):
try:
return HttpResponseForbidden(
render(
@ -71,7 +71,7 @@ def forbidden(request):
)
def not_found(request):
def not_found(request, exception):
return HttpResponseNotFound(render(request, "core/404.jinja"))
@ -81,32 +81,122 @@ def internal_servor_error(request):
def can_edit_prop(obj, user):
"""
:param obj: Object to test for permission
:param user: core.models.User to test permissions against
:return: if user is authorized to edit object properties
:rtype: bool
:Example:
.. code-block:: python
if not can_edit_prop(self.object ,request.user):
raise PermissionDenied
"""
if obj is None or user.is_owner(obj):
return True
return False
def can_edit(obj, user):
"""
:param obj: Object to test for permission
:param user: core.models.User to test permissions against
:return: if user is authorized to edit object
:rtype: bool
:Example:
.. code-block:: python
if not can_edit(self.object ,request.user):
raise PermissionDenied
"""
if obj is None or user.can_edit(obj):
return True
return can_edit_prop(obj, user)
def can_view(obj, user):
"""
:param obj: Object to test for permission
:param user: core.models.User to test permissions against
:return: if user is authorized to see object
:rtype: bool
:Example:
.. code-block:: python
if not can_view(self.object ,request.user):
raise PermissionDenied
"""
if obj is None or user.can_view(obj):
return True
return can_edit(obj, user)
class GenericContentPermissionMixinBuilder(View):
"""
Used to build permission mixins
This view protect any child view that would be showing an object that is restricted based
on two properties
:prop permission_function: function to test permission with, takes an object and an user an return a bool
:prop raised_error: permission to be raised
:raises: raised_error
"""
permission_function = lambda obj, user: False
raised_error = PermissionDenied
@classmethod
def get_permission_function(cls, obj, user):
return cls.permission_function(obj, user)
def dispatch(self, request, *arg, **kwargs):
if hasattr(self, "get_object") and callable(self.get_object):
self.object = self.get_object()
if not self.get_permission_function(self.object, request.user):
raise self.raised_error
return super(GenericContentPermissionMixinBuilder, self).dispatch(
request, *arg, **kwargs
)
# If we get here, it's a ListView
queryset = self.get_queryset()
l_id = [o.id for o in queryset if self.get_permission_function(o, request.user)]
if not l_id and queryset.count() != 0:
raise self.raised_error
self._get_queryset = self.get_queryset
def get_qs(self2):
return self2._get_queryset().filter(id__in=l_id)
self.get_queryset = types.MethodType(get_qs, self)
return super(GenericContentPermissionMixinBuilder, self).dispatch(
request, *arg, **kwargs
)
class CanCreateMixin(View):
"""
This view is made to protect any child view that would create an object, and thus, that can not be protected by any
of the following mixin
:raises: PermissionDenied
"""
def dispatch(self, request, *arg, **kwargs):
res = super(CanCreateMixin, self).dispatch(request, *arg, **kwargs)
if not request.user.is_authenticated():
if not request.user.is_authenticated:
raise PermissionDenied
return res
@ -117,95 +207,46 @@ class CanCreateMixin(View):
raise PermissionDenied
class CanEditPropMixin(View):
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
"""
This view is made to protect any child view that would be showing some properties of an object that are restricted
to only the owner group of the given object.
In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the
object's owner_group
:raises: PermissionDenied
"""
def dispatch(self, request, *arg, **kwargs):
try:
self.object = self.get_object()
if can_edit_prop(self.object, request.user):
return super(CanEditPropMixin, self).dispatch(request, *arg, **kwargs)
return forbidden(request)
except:
pass
# If we get here, it's a ListView
l_id = [o.id for o in self.get_queryset() if can_edit_prop(o, request.user)]
if not l_id and self.get_queryset().count() != 0:
raise PermissionDenied
self._get_queryset = self.get_queryset
def get_qs(self2):
return self2._get_queryset().filter(id__in=l_id)
self.get_queryset = types.MethodType(get_qs, self)
return super(CanEditPropMixin, self).dispatch(request, *arg, **kwargs)
permission_function = can_edit_prop
class CanEditMixin(View):
class CanEditMixin(GenericContentPermissionMixinBuilder):
"""
This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the
object
:raises: PermissionDenied
"""
def dispatch(self, request, *arg, **kwargs):
try:
self.object = self.get_object()
if can_edit(self.object, request.user):
return super(CanEditMixin, self).dispatch(request, *arg, **kwargs)
return forbidden(request)
except:
pass
# If we get here, it's a ListView
l_id = [o.id for o in self.get_queryset() if can_edit(o, request.user)]
if not l_id and self.get_queryset().count() != 0:
raise PermissionDenied
self._get_queryset = self.get_queryset
def get_qs(self2):
return self2._get_queryset().filter(id__in=l_id)
self.get_queryset = types.MethodType(get_qs, self)
return super(CanEditMixin, self).dispatch(request, *arg, **kwargs)
permission_function = can_edit
class CanViewMixin(View):
class CanViewMixin(GenericContentPermissionMixinBuilder):
"""
This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of
the object
:raises: PermissionDenied
"""
def dispatch(self, request, *arg, **kwargs):
try:
self.object = self.get_object()
if can_view(self.object, request.user):
return super(CanViewMixin, self).dispatch(request, *arg, **kwargs)
return forbidden(request)
except:
pass
# If we get here, it's a ListView
queryset = self.get_queryset()
l_id = [o.id for o in queryset if can_view(o, request.user)]
if not l_id and queryset.count() != 0:
raise PermissionDenied
self._get_queryset = self.get_queryset
def get_qs(self2):
return self2._get_queryset().filter(id__in=l_id)
self.get_queryset = types.MethodType(get_qs, self)
return super(CanViewMixin, self).dispatch(request, *arg, **kwargs)
permission_function = can_view
class FormerSubscriberMixin(View):
"""
This view check if the user was at least an old subscriber
:raises: PermissionDenied
"""
def dispatch(self, request, *args, **kwargs):
@ -214,6 +255,19 @@ class FormerSubscriberMixin(View):
return super(FormerSubscriberMixin, self).dispatch(request, *args, **kwargs)
class UserIsLoggedMixin(View):
"""
This view check if the user is logged
:raises: PermissionDenied
"""
def dispatch(self, request, *args, **kwargs):
if request.user.is_anonymous:
raise PermissionDenied
return super(UserIsLoggedMixin, self).dispatch(request, *args, **kwargs)
class TabedViewMixin(View):
"""
This view provide the basic functions for displaying tabs in the template

View File

@ -23,7 +23,7 @@
#
# This file contains all the views that concern the page model
from django.shortcuts import redirect
from django.shortcuts import redirect, get_object_or_404
from django.views.generic import ListView, DetailView, TemplateView
from django.views.generic.edit import UpdateView, FormMixin, DeleteView
from django.views.generic.detail import SingleObjectMixin
@ -32,7 +32,7 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponse
from wsgiref.util import FileWrapper
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.exceptions import PermissionDenied
from django import forms
@ -51,9 +51,7 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"):
memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB.
"""
f = file_class.objects.filter(id=file_id).first()
if f is None or not f.file:
return not_found(request)
f = get_object_or_404(file_class, id=file_id)
if not (
can_view(f, request.user)
or (
@ -111,7 +109,7 @@ class AddFilesForm(forms.Form):
owner=owner,
is_folder=False,
mime_type=f.content_type,
size=f._size,
size=f.size,
)
try:
new_file.clean()
@ -291,7 +289,7 @@ class FileView(CanViewMixin, DetailView, FormMixin):
self.form = self.get_form() # The form handle only the file upload
files = request.FILES.getlist("file_field")
if (
request.user.is_authenticated()
request.user.is_authenticated
and request.user.can_edit(self.object)
and self.form.is_valid()
):

View File

@ -27,7 +27,7 @@ from django import forms
from django.conf import settings
from django.db import transaction
from django.templatetags.static import static
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.exceptions import ValidationError
from django.forms import (
CheckboxSelectMultiple,
@ -55,40 +55,22 @@ from PIL import Image
# Widgets
class SelectSingle(Select):
def render(self, name, value, attrs=None):
if attrs:
attrs["class"] = "select_single"
else:
attrs = {"class": "select_single"}
return super(SelectSingle, self).render(name, value, attrs)
class SelectMultiple(Select):
def render(self, name, value, attrs=None):
if attrs:
attrs["class"] = "select_multiple"
else:
attrs = {"class": "select_multiple"}
return super(SelectMultiple, self).render(name, value, attrs)
class SelectDateTime(DateTimeInput):
def render(self, name, value, attrs=None):
def render(self, name, value, attrs=None, renderer=None):
if attrs:
attrs["class"] = "select_datetime"
else:
attrs = {"class": "select_datetime"}
return super(SelectDateTime, self).render(name, value, attrs)
return super(SelectDateTime, self).render(name, value, attrs, renderer)
class SelectDate(DateInput):
def render(self, name, value, attrs=None):
def render(self, name, value, attrs=None, renderer=None):
if attrs:
attrs["class"] = "select_date"
else:
attrs = {"class": "select_date"}
return super(SelectDate, self).render(name, value, attrs)
return super(SelectDate, self).render(name, value, attrs, renderer)
class MarkdownInput(Textarea):
@ -127,7 +109,7 @@ class MarkdownInput(Textarea):
class SelectFile(TextInput):
def render(self, name, value, attrs=None):
def render(self, name, value, attrs=None, renderer=None):
if attrs:
attrs["class"] = "select_file"
else:
@ -135,7 +117,7 @@ class SelectFile(TextInput):
output = (
'%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>'
% {
"content": super(SelectFile, self).render(name, value, attrs),
"content": super(SelectFile, self).render(name, value, attrs, renderer),
"title": _("Choose file"),
"name": name,
}
@ -151,7 +133,7 @@ class SelectFile(TextInput):
class SelectUser(TextInput):
def render(self, name, value, attrs=None):
def render(self, name, value, attrs=None, renderer=None):
if attrs:
attrs["class"] = "select_user"
else:
@ -159,7 +141,7 @@ class SelectUser(TextInput):
output = (
'%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>'
% {
"content": super(SelectUser, self).render(name, value, attrs),
"content": super(SelectUser, self).render(name, value, attrs, renderer),
"title": _("Choose user"),
"name": name,
}
@ -302,7 +284,7 @@ class UserProfileForm(forms.ModelForm):
owner=self.instance,
is_folder=False,
mime_type=f.content_type,
size=f._size,
size=f.size,
moderator=self.instance,
is_moderated=True,
)

View File

@ -29,7 +29,7 @@
from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.views.generic import ListView
from django.views.generic.edit import FormView
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django import forms

View File

@ -23,7 +23,7 @@
#
# This file contains all the views that concern the page model
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.forms.models import modelform_factory

View File

@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
@ -40,11 +41,9 @@ from club.models import Club
def index(request, context=None):
if request.user.is_authenticated():
from com.views import NewsListView
from com.views import NewsListView
return NewsListView.as_view()(request)
return render(request, "core/index.jinja")
return NewsListView.as_view()(request)
class NotificationList(ListView):
@ -52,8 +51,9 @@ class NotificationList(ListView):
template_name = "core/notification_list.jinja"
def get_queryset(self):
# TODO: Bulk update in django 2.2
if "see_all" in self.request.GET.keys():
for n in self.request.user.notifications.all():
for n in self.request.user.notifications.filter(viewed=False):
n.viewed = True
n.save()
return self.request.user.notifications.order_by("-date")[:20]

View File

@ -26,8 +26,9 @@
# This file contains all the views that concern the user model
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import views
from django.contrib.auth.forms import PasswordChangeForm
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.exceptions import PermissionDenied, ValidationError
from django.http import Http404, HttpResponse
from django.views.generic.edit import UpdateView
@ -40,7 +41,7 @@ from django.views.generic import (
)
from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple
from django.core.urlresolvers import reverse_lazy
from django.urls import reverse_lazy
from django.template.response import TemplateResponse
from django.conf import settings
from django.views.generic.dates import YearMixin, MonthMixin
@ -52,6 +53,7 @@ from core.views import (
CanViewMixin,
CanEditMixin,
CanEditPropMixin,
UserIsLoggedMixin,
TabedViewMixin,
QuickNotifMixin,
)
@ -68,15 +70,31 @@ from counter.views import StudentCardForm
from trombi.views import UserTrombiForm
def login(request):
class SithLoginView(views.LoginView):
"""
The login View
"""
The login view
Needs to be improve with correct handling of form exceptions
template_name = "core/login.jinja"
authentication_form = LoginForm
form_class = PasswordChangeForm
class SithPasswordChangeView(views.PasswordChangeView):
"""
return views.login(
request, template_name="core/login.jinja", authentication_form=LoginForm
)
Allows a user to change its password
"""
template_name = "core/password_change.jinja"
success_url = reverse_lazy("core:password_change_done")
class SithPasswordChangeDoneView(views.PasswordChangeDoneView):
"""
Allows a user to change its password
"""
template_name = "core/password_change_done.jinja"
def logout(request):
@ -86,26 +104,6 @@ def logout(request):
return views.logout_then_login(request)
def password_change(request):
"""
Allows a user to change its password
"""
return views.password_change(
request,
template_name="core/password_change.jinja",
post_change_redirect=reverse("core:password_change_done"),
)
def password_change_done(request):
"""
Allows a user to change its password
"""
return views.password_change_done(
request, template_name="core/password_change_done.jinja"
)
def password_root_change(request, user_id):
"""
Allows a root user to change someone's password
@ -127,47 +125,39 @@ def password_root_change(request, user_id):
)
def password_reset(request):
class SithPasswordResetView(views.PasswordResetView):
"""
Allows someone to enter an email adresse for resetting password
"""
return views.password_reset(
request,
template_name="core/password_reset.jinja",
email_template_name="core/password_reset_email.jinja",
post_reset_redirect="core:password_reset_done",
)
template_name = "core/password_reset.jinja"
email_template_name = "core/password_reset_email.jinja"
success_url = reverse_lazy("core:password_reset_done")
def password_reset_done(request):
class SithPasswordResetDoneView(views.PasswordResetDoneView):
"""
Confirm that the reset email has been sent
"""
return views.password_reset_done(
request, template_name="core/password_reset_done.jinja"
)
template_name = "core/password_reset_done.jinja"
def password_reset_confirm(request, uidb64=None, token=None):
class SithPasswordResetConfirmView(views.PasswordResetConfirmView):
"""
Provide a reset password formular
Provide a reset password form
"""
return views.password_reset_confirm(
request,
uidb64=uidb64,
token=token,
post_reset_redirect="core:password_reset_complete",
template_name="core/password_reset_confirm.jinja",
)
template_name = "core/password_reset_confirm.jinja"
success_url = reverse_lazy("core:password_reset_complete")
def password_reset_complete(request):
class SithPasswordResetCompleteView(views.PasswordResetCompleteView):
"""
Confirm the password has sucessfully been reset
"""
return views.password_reset_complete(
request, template_name="core/password_reset_complete.jinja"
)
template_name = "core/password_reset_complete.jinja"
def register(request):
@ -640,7 +630,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView):
owner=self.object,
is_folder=False,
mime_type=f.content_type,
size=f._size,
size=f.size,
)
new_file.file.name = name
new_file.save()
@ -688,7 +678,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView):
files = request.FILES.items()
self.form.process(files)
if (
request.user.is_authenticated()
request.user.is_authenticated
and request.user.can_edit(self.object)
and self.form.is_valid()
):
@ -742,7 +732,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
kwargs = super(UserPreferencesView, self).get_context_data(**kwargs)
if not hasattr(self.object, "trombi_user"):
kwargs["trombi_form"] = UserTrombiForm()
if self.object.customer:
if hasattr(self.object, "customer"):
kwargs["student_card_form"] = StudentCardForm()
return kwargs
@ -762,7 +752,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView):
current_tab = "groups"
class UserToolsView(QuickNotifMixin, UserTabsMixin, TemplateView):
class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView):
"""
Displays the logged user's tools
"""

View File

@ -5,6 +5,7 @@ from django.db import migrations, models
import accounting.models
import django.db.models.deletion
from django.conf import settings
import django.db.models.deletion
class Migration(migrations.Migration):
@ -42,7 +43,14 @@ class Migration(migrations.Migration):
verbose_name="counter type",
),
),
("club", models.ForeignKey(to="club.Club", related_name="counters")),
(
"club",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="club.Club",
related_name="counters",
),
),
(
"edit_groups",
models.ManyToManyField(
@ -58,7 +66,10 @@ class Migration(migrations.Migration):
(
"user",
models.OneToOneField(
primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to=settings.AUTH_USER_MODEL,
),
),
(
@ -97,13 +108,17 @@ class Migration(migrations.Migration):
(
"counter",
models.ForeignKey(
to="counter.Counter", related_name="permanencies"
on_delete=django.db.models.deletion.CASCADE,
to="counter.Counter",
related_name="permanencies",
),
),
(
"user",
models.ForeignKey(
to=settings.AUTH_USER_MODEL, related_name="permanencies"
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
related_name="permanencies",
),
),
],
@ -169,7 +184,10 @@ class Migration(migrations.Migration):
(
"club",
models.ForeignKey(
verbose_name="club", to="club.Club", related_name="products"
on_delete=django.db.models.deletion.CASCADE,
verbose_name="club",
to="club.Club",
related_name="products",
),
),
(
@ -268,15 +286,24 @@ class Migration(migrations.Migration):
),
(
"counter",
models.ForeignKey(to="counter.Counter", related_name="refillings"),
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="counter.Counter",
related_name="refillings",
),
),
(
"customer",
models.ForeignKey(to="counter.Customer", related_name="refillings"),
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="counter.Customer",
related_name="refillings",
),
),
(
"operator",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
related_name="refillings_as_operator",
),

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import accounting.models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -35,6 +36,7 @@ class Migration(migrations.Migration):
(
"counter",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="counter.Counter",
related_name="cash_summaries",
verbose_name="counter",
@ -43,6 +45,7 @@ class Migration(migrations.Migration):
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
related_name="cash_summaries",
verbose_name="user",
@ -74,6 +77,7 @@ class Migration(migrations.Migration):
(
"cash_summary",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="counter.CashRegisterSummary",
related_name="items",
verbose_name="cash summary",
@ -86,6 +90,7 @@ class Migration(migrations.Migration):
model_name="permanency",
name="counter",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="counter.Counter",
related_name="permanencies",
verbose_name="counter",
@ -95,6 +100,7 @@ class Migration(migrations.Migration):
model_name="permanency",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
related_name="permanencies",
verbose_name="user",

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -13,7 +14,10 @@ class Migration(migrations.Migration):
model_name="counter",
name="club",
field=models.ForeignKey(
verbose_name="club", to="club.Club", related_name="counters"
on_delete=django.db.models.deletion.CASCADE,
verbose_name="club",
to="club.Club",
related_name="counters",
),
),
migrations.AlterField(

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -32,6 +33,7 @@ class Migration(migrations.Migration):
(
"product",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
verbose_name="product",
related_name="eticket",
to="counter.Product",

View File

@ -26,7 +26,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.conf import settings
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.validators import MinLengthValidator
from django.forms import ValidationError
from django.utils.functional import cached_property
@ -51,7 +51,7 @@ class Customer(models.Model):
is used by other accounting classes as reference to the customer, rather than using User
"""
user = models.OneToOneField(User, primary_key=True)
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
account_id = models.CharField(_("account id"), max_length=10, unique=True)
amount = CurrencyField(_("amount"))
recorded_products = models.IntegerField(_("recorded product"), default=0)
@ -163,7 +163,9 @@ class Product(models.Model):
icon = models.ImageField(
upload_to="products", null=True, blank=True, verbose_name=_("icon")
)
club = models.ForeignKey(Club, related_name="products", verbose_name=_("club"))
club = models.ForeignKey(
Club, related_name="products", verbose_name=_("club"), on_delete=models.CASCADE
)
limit_age = models.IntegerField(_("limit age"), default=0)
tray = models.BooleanField(_("tray price"), default=False)
parent_product = models.ForeignKey(
@ -209,7 +211,9 @@ class Product(models.Model):
class Counter(models.Model):
name = models.CharField(_("name"), max_length=30)
club = models.ForeignKey(Club, related_name="counters", verbose_name=_("club"))
club = models.ForeignKey(
Club, related_name="counters", verbose_name=_("club"), on_delete=models.CASCADE
)
products = models.ManyToManyField(
Product, related_name="counters", verbose_name=_("products"), blank=True
)
@ -344,12 +348,19 @@ class Refilling(models.Model):
Handle the refilling
"""
counter = models.ForeignKey(Counter, related_name="refillings", blank=False)
counter = models.ForeignKey(
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
)
amount = CurrencyField(_("amount"))
operator = models.ForeignKey(
User, related_name="refillings_as_operator", blank=False
User,
related_name="refillings_as_operator",
blank=False,
on_delete=models.CASCADE,
)
customer = models.ForeignKey(
Customer, related_name="refillings", blank=False, on_delete=models.CASCADE
)
customer = models.ForeignKey(Customer, related_name="refillings", blank=False)
date = models.DateTimeField(_("date"))
payment_method = models.CharField(
_("payment method"),
@ -467,6 +478,10 @@ class Selling(models.Model):
return user.is_owner(self.counter) and self.payment_method != "CARD"
def can_be_viewed_by(self, user):
if (
not hasattr(self, "customer") or self.customer is None
): # Customer can be set to Null
return False
return user == self.customer.user
def delete(self, *args, **kwargs):
@ -479,7 +494,7 @@ class Selling(models.Model):
event = self.product.eticket.event_title or _("Unknown event")
subject = _("Eticket bought for the event %(event)s") % {"event": event}
message_html = _(
"You bought an eticket for the event %(event)s.\nYou can download it on this page %(url)s."
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
) % {
"event": event,
"url": "".join(
@ -491,10 +506,23 @@ class Selling(models.Model):
"</a>",
)
),
"eticket": "".join(
(
'<a href="',
self.get_eticket_full_url(),
'">',
self.get_eticket_full_url(),
"</a>",
)
),
}
message_txt = _(
"You bought an eticket for the event %(event)s.\nYou can download it on this page %(url)s."
) % {"event": event, "url": self.customer.get_full_url()}
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
) % {
"event": event,
"url": self.customer.get_full_url(),
"eticket": self.get_eticket_full_url(),
}
self.customer.user.email_user(subject, message_txt, html_message=message_html)
def save(self, allow_negative=False, *args, **kwargs):
@ -557,11 +585,6 @@ class Selling(models.Model):
start=sub.subscription_start,
)
sub.save()
try:
if self.product.eticket:
self.send_mail_customer()
except:
pass
if self.customer.user.preferences.notify_on_click:
Notification(
user=self.customer.user,
@ -577,6 +600,16 @@ class Selling(models.Model):
type="SELLING",
).save()
super(Selling, self).save(*args, **kwargs)
try:
# The product has no id until it's saved
if self.product.eticket:
self.send_mail_customer()
except:
pass
def get_eticket_full_url(self):
eticket_url = reverse("counter:eticket_pdf", kwargs={"selling_id": self.id})
return "".join(["https://", settings.SITH_URL, eticket_url])
class Permanency(models.Model):
@ -584,9 +617,17 @@ class Permanency(models.Model):
This class aims at storing a traceability of who was barman where and when
"""
user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user"))
user = models.ForeignKey(
User,
related_name="permanencies",
verbose_name=_("user"),
on_delete=models.CASCADE,
)
counter = models.ForeignKey(
Counter, related_name="permanencies", verbose_name=_("counter")
Counter,
related_name="permanencies",
verbose_name=_("counter"),
on_delete=models.CASCADE,
)
start = models.DateTimeField(_("start date"))
end = models.DateTimeField(_("end date"), null=True, db_index=True)
@ -607,10 +648,16 @@ class Permanency(models.Model):
class CashRegisterSummary(models.Model):
user = models.ForeignKey(
User, related_name="cash_summaries", verbose_name=_("user")
User,
related_name="cash_summaries",
verbose_name=_("user"),
on_delete=models.CASCADE,
)
counter = models.ForeignKey(
Counter, related_name="cash_summaries", verbose_name=_("counter")
Counter,
related_name="cash_summaries",
verbose_name=_("counter"),
on_delete=models.CASCADE,
)
date = models.DateTimeField(_("date"))
comment = models.TextField(_("comment"), null=True, blank=True)
@ -683,7 +730,10 @@ class CashRegisterSummary(models.Model):
class CashRegisterSummaryItem(models.Model):
cash_summary = models.ForeignKey(
CashRegisterSummary, related_name="items", verbose_name=_("cash summary")
CashRegisterSummary,
related_name="items",
verbose_name=_("cash summary"),
on_delete=models.CASCADE,
)
value = CurrencyField(_("value"))
quantity = models.IntegerField(_("quantity"), default=0)
@ -699,7 +749,10 @@ class Eticket(models.Model):
"""
product = models.OneToOneField(
Product, related_name="eticket", verbose_name=_("product")
Product,
related_name="eticket",
verbose_name=_("product"),
on_delete=models.CASCADE,
)
banner = models.ImageField(
upload_to="etickets", null=True, blank=True, verbose_name=_("banner")
@ -772,4 +825,5 @@ class StudentCard(models.Model):
verbose_name=_("student cards"),
null=False,
blank=False,
on_delete=models.CASCADE,
)

View File

@ -23,7 +23,7 @@
{% endblock %}
{% block content %}
<h4>{{ counter }}</h4>
<h4 id="click_interface">{{ counter }}</h4>
<div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5>
@ -127,17 +127,14 @@
</div>
<div id="products">
<ul>
{% for t in categories -%}
{%- if counter.products.filter(product_type=t).exists() -%}
<li><a href="#cat_{{ t|slugify }}">{{ t }}</a></li>
{%- endif -%}
{% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</ul>
{% for t in categories -%}
{%- if counter.products.filter(product_type=t).exists() -%}
<div id="cat_{{ t|slugify }}">
<h5>{{ t }}</h5>
{% for p in counter.products.filter(product_type=t).all() -%}
{% for category in categories.keys() -%}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
@ -148,18 +145,20 @@
{{ add_product(p.id, prod, "form_button") }}
{%- endfor %}
</div>
{%- endif -%}
{%- endfor %}
</div>
{% endblock %}
{% block script %}
<script>
document.getElementById("click_interface").scrollIntoView();
</script>
{{ super() }}
<script>
$( function() {
var products = [
{% for p in counter.products.all() -%}
{% for p in products -%}
{
value: "{{ p.code }}",
label: "{{ p.name }}",
@ -203,6 +202,3 @@ $( function() {
});
</script>
{% endblock %}

View File

@ -10,7 +10,7 @@
<h3>{% trans %}Eticket list{% endtrans %}</h3>
<ul>
{% for t in eticket_list %}
<li><a href="{{ url('counter:edit_eticket', eticket_id=t.id) }}">{{ t }}</a> (Hash: <code>{{ t.secret }}</code>)</li>
<li><a href="{{ url('counter:edit_eticket', eticket_id=t.id) }}">{{ t }}</a> (ID: {{ t.product.id }} | Hash: <code>{{ t.secret }}</code>)</li>
{% endfor %}
</ul>
{% else %}

View File

@ -25,7 +25,7 @@
import re
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.management import call_command
from core.models import User
@ -83,6 +83,17 @@ class CounterTest(TestCase):
)
class CounterStatsTest(TestCase):
def setUp(self):
call_command("populate")
self.counter = Counter.objects.filter(id=2).first()
def test_unothorized_user_fail(self):
# Test with not login user
response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
self.assertTrue(response.status_code == 403)
class BarmanConnectionTest(TestCase):
def setUp(self):
call_command("populate")

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