1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-01 13:31:16 +02:00

Compare commits

..

199 Commits

Author SHA1 Message Date
8f895dc4d7 note: use native selector rather than Query 2021-05-12 18:03:44 +02:00
1187577728 Do not lint scripts submodule 2021-05-12 17:33:11 +02:00
8a58af3b31 Reformat apps/registration/tokens.py 2021-05-05 19:48:19 +02:00
0c23625147 Remove newline in imports 2021-05-05 19:47:16 +02:00
21219b9c62 Rename join_BDE and join_Kfet to lowercase 2021-05-05 19:46:53 +02:00
5ab8beecef Use _ prefix for ignored loop variable 2021-05-05 19:14:59 +02:00
1ca5133026 BaseException.message is removed in Python 3 2021-05-05 19:12:23 +02:00
93bc6bb245 Do not call setattr with a constant attribute value 2021-05-05 19:12:03 +02:00
952c4383e7 Add flake8-bugbear and lint all apps 2021-05-05 19:09:33 +02:00
15dd2b8f0c PC Kfet can update profile section while renewing memberships
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-29 13:11:00 +02:00
c540b6334c Fix minimum amount for the send_mail_to_negative_balances script
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-27 09:52:34 +02:00
0b93968b9e Merge branch 'docs' into 'beta'
More and more documentation

See merge request bde/nk20!156
2021-04-23 19:10:56 +00:00
97375ef6c0 Copy production database to development website
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:57 +02:00
36cfcd533f Documentation on scripts
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:56 +02:00
21dbc53615 Spam negative users if they have less than 0 euro
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:53 +02:00
e6f10ebdac Install production server
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
47968844ce Install local development server
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
a435460e29 Personal interface
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
b7c4360108 How to NK20
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
8d8c417c50 Fix old markdown remaining in docs 2021-04-23 21:07:28 +02:00
2b189af25b [FAQ] How to create a button?
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-23 21:07:28 +02:00
5a07c8a94f More FAQ
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
6cc1857eb6 Add FAQ page
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
601534d610 Add link to the documentatioh on the README
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
c271593839 Fix markdown typos
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
f351794aa0 Documentation on documentation
Signed-off-by: ynerant <ynerant@crans.org>
2021-04-23 21:07:27 +02:00
2793fee58c Merge branch 'less-verbosity' into 'beta'
Reduce verbosity of cron scripts

See merge request bde/nk20!161
2021-04-14 13:57:55 +00:00
7a715df121 Treasurers asked to add the list of old members in the monthly reports
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 15:48:15 +02:00
9308878054 Adapt verbosity of some scripts
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 15:18:49 +02:00
b5ccf5b800 Run cron without verbosity
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 15:05:13 +02:00
5e63254439 Merge branch 'fix-reports' into 'beta'
Update last report date only in non-debug mode

See merge request bde/nk20!160
2021-04-14 13:03:04 +00:00
da96506218 Update last report date only in non-debug mode
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-14 14:51:24 +02:00
b4714b896a Merge branch 'fix-reports' into 'beta'
Daily reports were broken

See merge request bde/nk20!159
2021-04-08 17:45:17 +00:00
cdb2647a4d Fix note list when daily reports are sent
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-08 17:33:51 +02:00
cc12e3ec63 Send cron mails to the BDE
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-04-08 17:31:01 +02:00
be168c5ada Decimal value is serialized as a str value
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-21 10:59:58 +01:00
b46ae6f856 [treasury] Product quantities are finally decimal fields 2021-03-21 10:41:15 +01:00
ec0bcbf015 PC Kfet can see all users
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-21 10:28:50 +01:00
81303b8ef8 Merge branch 'floating-quantities' into 'beta'
[invoices] Product quantities can be floating

See merge request bde/nk20!158
2021-03-13 12:13:50 +00:00
910b98fefc Invoices are in french
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-13 13:04:00 +01:00
5a7a219ba8 [invoices] Quantities can be non-integers
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-13 12:35:28 +01:00
116451603c Merge branch 'cas' into 'beta'
CAS + OAuth2

See merge request bde/nk20!155
2021-03-09 16:39:03 +00:00
b2437ef9b5 Remove additional blank lines
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 17:18:43 +01:00
d8c9618772 OAuth2 documentation
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 17:04:16 +01:00
c825dee95a CAS documentation
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 15:42:36 +01:00
73d27e820b Provide also note information (with balance and picture)
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 12:55:19 +01:00
40e1b42078 Fix API path
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 12:54:57 +01:00
72806f0ace Add profile and membership information to OAuth views
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 10:57:35 +01:00
b244e01231 Add simple view to give OAuth information
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 10:41:43 +01:00
76d1784aea Add OAuth2 authentication for Django Rest Framework
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 09:44:25 +01:00
56c5fa4057 We don't need a session to have permissions
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 09:41:27 +01:00
b5ef937a03 Environment file path is absolute
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-09 09:39:57 +01:00
e95a8b6e18 Add normalized name to services
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-03 18:42:51 +01:00
635adf1360 Use cas server to use authentication in other services
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-03-03 18:13:33 +01:00
d5a9bf175f Add script to force delete a user, in case of duplicates
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-02-22 11:54:23 +01:00
b597a6ac5b Fix soge credit deletion when the account is not validated yet
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2021-02-21 23:05:27 +01:00
a704b92c3d Prez BDE : ajout transaction random + see all buttons 2021-02-20 15:12:08 +01:00
53090b1a21 Fix JS texts
Signed-off-by: ynerant <ynerant@crans.org>
2021-02-14 11:52:37 +01:00
c49af0b83a Merge branch 'beta' into 'master'
Fix memberships

See merge request bde/nk20!147
2021-02-11 20:49:30 +00:00
5a05997d9d Fix date comparison when checking a membership from the parent club
Signed-off-by: ynerant <ynerant@crans.org>
2021-02-11 21:38:44 +01:00
c109cd3ddd Source is not destination
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 15:17:03 +01:00
84304971d7 Add sample translation file for english
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 14:29:12 +01:00
b8b781f9a2 Merge branch 'beta' into 'master'
Beta

Closes #84 et #83

See merge request bde/nk20!146
2021-01-19 12:40:24 +01:00
002128eed2 Merge branch 'fix-js-strings' into 'master'
Fix js strings

Closes #85, #84 et #83

See merge request bde/nk20!144
2021-01-19 12:24:13 +01:00
8d71783c42 Merge branch 'docs' into 'beta'
Docs

See merge request bde/nk20!145
2021-01-19 12:01:45 +01:00
a6f23df7d5 Load the good translation file, fixes #85
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2021-01-19 11:58:19 +01:00
d9c97628e2 Add Clacks Overhead header on each response. Closes #84
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-31 15:40:18 +01:00
893534955d Use the Debian mirror of Crans
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-30 00:04:08 +01:00
dfbf9972c2 By default, automatically change directory to /var/www/note_kfet and source the Python virtual environment in the .bashrc file
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 23:27:51 +01:00
b5f3b3ffc1 Use Nginx certbot challenge
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 21:44:28 +01:00
3aad4e7398 Agree Let's Encrypt ToS
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 21:41:29 +01:00
b4a1b513cc Good bye bde3-virt, welcome bde-note-dev!
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-29 20:05:15 +01:00
c0c64f225c Merge branch 'ansible-fix' into 'beta'
Ansible fix

See merge request bde/nk20!139
2020-12-29 20:01:43 +01:00
9d8f47115c ConsumerViewSet is a bit tricky
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:50:48 +01:00
f4156f1b94 Update API links, more detail on filtering
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:42:58 +01:00
e60994e065 API Documentation
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 21:06:30 +01:00
801f711994 Merge branch 'beta' into docs 2020-12-23 20:19:40 +01:00
e4568b410f How to authenticate on the API?
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 19:19:46 +01:00
c8f7986d5a Merge branch 'api' into 'beta'
API Filters

See merge request bde/nk20!143
2020-12-23 19:02:59 +01:00
d3a9c442a5 Test the note kfet with Debian Bullseye, Python 3.9 and Django 2.2
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:48:09 +01:00
016ab5a9c9 Remove dead code, don't try to cover unnecessary things
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:45:05 +01:00
7866ab7ec0 Ordering filters are now properly tested
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:25:54 +01:00
f570ff3cd5 Check that permissions are working when accessing to API pages
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 18:21:59 +01:00
6b2638c271 Documentation is located on /doc
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 15:20:30 +01:00
5cb4183e9f Use python Warnings instead of printing messages during tests
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 15:11:33 +01:00
3a20555663 Unit tests for API pages, closes #83
Signed-off-by: Yohann D'ANELLO <yohann.danello@gmail.com>
2020-12-23 14:54:21 +01:00
95be0042e9 Fix transaction API page 2020-12-22 13:28:43 +01:00
48880e7fd3 More API filters for the wei app 2020-12-22 13:11:01 +01:00
e0030771e4 More API filters for the treasury app 2020-12-22 12:53:35 +01:00
d47799e6ee More API filters for the permission app 2020-12-22 12:42:54 +01:00
eae091625a More API filters for the note app 2020-12-22 12:37:21 +01:00
aceb77ffb9 More API filters for the activity app 2020-12-22 03:18:43 +01:00
338c94ed05 More API filters for the member app 2020-12-22 02:58:12 +01:00
290848f904 Non-member people can update their profile everytime 2020-12-02 14:58:14 +01:00
72dca54bbf Wrong path for artifact uploading 2020-11-26 03:13:57 +01:00
117d9da3ba Gitlab compiles the documentation 2020-11-26 02:55:36 +01:00
37efebe85b Ansible builds and deploys the documentation 2020-11-26 02:49:57 +01:00
3af2ec71b6 Extract documentation from the Gitlab wiki and convert it to reStructuredText 2020-11-26 02:29:51 +01:00
0b4a95525b Use RTD theme 2020-11-26 01:07:18 +01:00
af664e481f Initial setup for Sphinx 2020-11-26 01:01:08 +01:00
0171f16311 Merge branch 'beta' into 'master'
Translate some words

See merge request bde/nk20!142
2020-11-21 13:55:26 +01:00
296b94d237 Merge branch 'master' into 'beta'
Translations

See merge request bde/nk20!141
2020-11-21 13:37:24 +01:00
4942553335 Merge branch 'JS_translations' into 'master'
Js translations

Closes #73

See merge request bde/nk20!140
2020-11-21 13:18:32 +01:00
c1efb87180 Fix spanish translations 2020-11-21 12:37:31 +01:00
72eead8595 Add spanish javascript translation 2020-11-21 12:26:31 +01:00
ade7e583e5 Complete spanish translation to 98% 2020-11-21 12:24:55 +01:00
4a8a101822 Translated using Weblate (German)
Currently translated at 100.0% (19 of 19 strings)

Translation: Note Kfet 2020/NK20 - JS
Translate-URL: http://translate.ynerant.fr/projects/nk20/nk20-js/de/
2020-11-16 20:21:46 +00:00
dd2cfa6327 Translated using Weblate (French)
Currently translated at 100.0% (668 of 668 strings)

Translation: Note Kfet 2020/NK20
Translate-URL: http://translate.ynerant.fr/projects/nk20/nk20/fr/
2020-11-16 20:02:32 +00:00
2adf84b7fc Translated using Weblate (German)
Currently translated at 98.0% (655 of 668 strings)

Translation: Note Kfet 2020/NK20
Translate-URL: http://translate.ynerant.fr/projects/nk20/nk20/de/
2020-11-16 20:02:31 +00:00
2f54e64ea2 Merge branch 'JS_translations' into 'beta'
Js translations

See merge request bde/nk20!125
2020-11-16 20:49:46 +01:00
8434c0062c Merge branch 'beta' into JS_translations
# Conflicts:
#	apps/note/static/note/js/consos.js
#	locale/de/LC_MESSAGES/django.po
#	locale/es/LC_MESSAGES/django.po
#	locale/fr/LC_MESSAGES/django.po
2020-11-16 00:59:26 +01:00
6d976f32bf Update django oauth toolkit, fix #73 2020-11-16 00:49:53 +01:00
b9d49d53f2 Export JS translation files as static files 2020-11-16 00:29:27 +01:00
23243e09bb Fix some errors on JS string interpolation 2020-11-15 23:37:36 +01:00
2682e9a610 Add line in README on how to extract localized string in JS files 2020-11-15 23:31:10 +01:00
5635598bbc Extract strings from javascript files and translate them in french 2020-11-15 23:28:41 +01:00
b58a0c43cd Include auto-generated javascript translation file 2020-11-15 22:53:00 +01:00
e1f647bd02 lesser hardcoded 2020-10-30 21:28:25 +01:00
39fd3a2471 set DB_PASSWORD in env file 2020-10-30 20:54:41 +01:00
1072e227b8 don't copy personal config on prod 2020-10-30 17:07:03 +01:00
cbf7e6fe6c run certbot if necessary 2020-10-30 17:01:47 +01:00
950922d041 do not hardcode mail 2020-10-30 17:01:26 +01:00
78fe070cd3 use debian backport only with debian 2020-10-30 16:59:44 +01:00
51d5733578 less hardcoded ansible config 2020-10-30 16:58:49 +01:00
7bd895c1df Grant treasurers to update a note picture 2020-10-26 17:58:30 +01:00
e5e94c52f2 Merge branch 'beta' into 'master'
Permissions PC Kfet

See merge request bde/nk20!138
2020-10-25 22:08:00 +01:00
051591cb7a Don't see user detail in update form 2020-10-25 21:49:16 +01:00
0e7390b669 PC Kfet can see limited user information and clubs. It can create memberships but not see them 2020-10-25 21:38:04 +01:00
fe4363b83d Don't display too much detail when a user has no right to see a profile 2020-10-25 21:29:44 +01:00
6e80016b38 Don't delete object when checking an add permission: this is useless since we rollback to the initial DB state 2020-10-25 21:08:36 +01:00
08e50ffc22 Credit form didn't raise an error when the data didn't validate 2020-10-23 18:19:21 +02:00
9cb65277f3 Merge branch 'beta' into 'master'
Ajustement de permissions

See merge request bde/nk20!137
2020-10-23 17:10:15 +02:00
224a0fdd8c SpecialTransactionProxy are force-saved 2020-10-23 16:55:33 +02:00
6dc7604e90 Alias were duplicated in profile alias list view 2020-10-23 16:48:33 +02:00
cb7f3c9f18 Note account can manage BDE memberships 2020-10-23 16:42:06 +02:00
f910feca9e PC Kfet can create and renew memberships 2020-10-23 13:17:07 +02:00
91f784872c Treasurers can update any roles, not only the BDE-related 2020-10-23 09:50:18 +02:00
b655135a42 Merge branch 'beta' into 'master'
PC Kfet

See merge request bde/nk20!136
2020-10-20 10:43:01 +02:00
58aa4983e3 The note account must be active in order to have access to the Rest Framework API 2020-10-20 10:30:41 +02:00
6cc3cf4174 A migration put the right role in the note account's memberships 2020-10-20 00:28:49 +02:00
2097e67321 Add permissions to PC Kfet 2020-10-20 00:19:49 +02:00
d773303d18 Add possibility to authenticate an account with its IP address 2020-10-19 23:44:56 +02:00
3cabcf40e7 Merge branch 'beta' into 'master'
Display real user name in the Soge credits list/detail

See merge request bde/nk20!135
2020-10-08 10:48:36 +02:00
bf29efda0a Display real user name in the Soge credits list/detail 2020-10-08 10:36:30 +02:00
ceccba0d71 Merge branch 'beta' into 'master'
Highlight users that created a bank account

See merge request bde/nk20!134
2020-10-07 17:55:40 +02:00
3eced33082 Well, everyone doesn't want a secondary bank account 2020-10-07 17:43:28 +02:00
acb3fb4a91 Highlight future users that declared that they opened a bank account 2020-10-07 17:42:46 +02:00
1c5e951c2f Merge branch 'beta' into 'master'
Various fixes

See merge request bde/nk20!133
2020-10-07 12:06:48 +02:00
beb1853aef Forgot to create the aliases for BDE and Kfet in the migration that create the clubs 2020-10-07 11:54:04 +02:00
0078eb8f90 Index page is a redirection 2020-10-07 11:53:42 +02:00
e5e758f9d9 Display banners when a user is no more a BDE or Kfet member 2020-10-07 11:46:43 +02:00
4a78328717 The checkbox to tell that a Sogé account got opened is not mandatory 2020-10-07 11:31:20 +02:00
65a2e8c08c Better index page: non-Kfet members will be redirected to their profile page, the account note (when it will be managed) will see the consumption page 2020-10-07 11:29:52 +02:00
b5fa428bad Non-Kfet members can see their old aliases only, but no one else 2020-10-07 11:22:02 +02:00
fb72385773 Warn users that they have to open they Sogé account 2020-10-07 10:59:37 +02:00
2f68601e8b Delete the soge credit if the user declares that one was opened but in the validation form the checkbox was unchecked 2020-10-07 10:46:33 +02:00
0b1bed8048 Temporary give the right to treasurers to manage membership roles, but need to find a proper solution 2020-10-07 10:43:58 +02:00
8ada0e51f2 The validation filter of the soge credit list was buggy 2020-10-07 10:42:52 +02:00
c3d613947f Pre-registered users can declare that they opened a bank account in the signup form 2020-10-07 10:33:57 +02:00
36b8157372 Fix membership table order 2020-10-07 10:03:43 +02:00
992cfe8e23 Can set a parent club to None 2020-10-07 09:48:21 +02:00
18a8ff1b8a Set credit/debit reason non mandatory 2020-10-07 09:45:09 +02:00
c61bb2e90d When we credit the note of a club directly, fill the last name and the first name information with the club name instead of empty 2020-10-07 09:39:40 +02:00
4b12e3ed08 Display only the most recent membership 2020-10-07 09:29:41 +02:00
af07ed9807 Merge branch 'erdnaxe-master-patch-99095' into 'beta'
Erdnaxe master patch 99095

See merge request bde/nk20!132
2020-10-05 16:37:14 +02:00
bbe53b3b63 Update README.md 2020-10-05 16:25:06 +02:00
536f0ec226 Merge branch 'beta' into 'master'
Ensure the integrity of all note balances in multiple transactions

See merge request bde/nk20!131
2020-10-04 22:12:12 +02:00
541ed59f40 When a membership is created, redirect to the user profile page rather than club detail 2020-10-04 21:08:35 +02:00
e172b4f4bb When a membership is renewed, set the same roles as the previous membership 2020-10-04 20:54:03 +02:00
d666179037 Display Renew membership button 15 days more 2020-10-04 20:50:10 +02:00
f22e92132c Select for update transaction notes, and not only the transaction 2020-10-04 20:47:15 +02:00
ca7ad05746 Use a signal to prevent a user that the note balance is negative 2020-10-04 20:26:43 +02:00
f55ca2f725 Merge branch 'beta' into 'master'
remove the display limit for pre-registred users.

See merge request bde/nk20!130
2020-10-03 18:07:39 +02:00
d4e4ed580f remove the display limit for pre-registred users. 2020-10-03 17:53:38 +02:00
8756751344 Merge branch 'beta' into 'master'
Fix some membership issues

See merge request bde/nk20!129
2020-10-01 15:00:36 +02:00
fd83fe19bf Fix some membership date control 2020-10-01 09:17:02 +02:00
a00d95608b Add permission to treasurers to create a club, fix the permission check to renew a membership 2020-09-23 21:36:04 +02:00
3303edd01f Merge branch 'beta' into 'master'
OAuth2, no side effect permissions

See merge request bde/nk20!128
2020-09-23 18:44:40 +02:00
e48ef92137 Revert commit that broke beta branch 2020-09-23 18:32:09 +02:00
919d0b7e85 Merge branch 'ics_cache' into 'beta'
Ics cache

See merge request bde/nk20!127
2020-09-21 15:46:34 +02:00
439bf35b62 APT python memcache is PyPi memcached 2020-09-21 15:25:07 +02:00
74b26335d1 Cache ICS calendar 2020-09-21 15:13:59 +02:00
3d733ed6af Use memcached cache 2020-09-21 15:13:43 +02:00
d54ab94ceb Merge branch 'oauth' into 'beta'
Oauth

See merge request bde/nk20!126
2020-09-21 12:53:03 +02:00
4f188ca3e5 Admin is autodiscovering partially 2020-09-21 12:34:34 +02:00
72bac75fbd Add Django OAuth toolkit admin 2020-09-21 12:15:40 +02:00
6d54aae614 Fix django-oauth-toolkit version 2020-09-21 11:15:00 +02:00
8052152ea5 Add OAuth2 endpoints 2020-09-21 11:03:07 +02:00
70448db8e5 Remove Django CAS server and add oauth toolkit 2020-09-21 10:31:42 +02:00
ac2d1e8111 Merge branch 'no_side_effect_permission_check' into 'beta'
No side effect permission check

See merge request bde/nk20!124
2020-09-20 11:24:46 +02:00
3ba61385a3 Debit is not credit 2020-09-20 11:12:44 +02:00
7353348d7a Rollback transaction when checking an add permission (experimental) 2020-09-20 09:07:51 +02:00
f63e2e088e Don't log when the permission to lock a note is checked 2020-09-20 08:56:42 +02:00
420a24ebac enable JavaScriptCatalog view 2020-09-19 22:42:35 +02:00
d566def706 Try to translate js, not working... 2020-09-19 22:03:45 +02:00
eaf6769e8b Treasurers can make transactions with people that are no longer a member 2020-09-19 16:33:52 +02:00
a61ec81cff note.crans.org is the default domain 2020-09-19 16:03:32 +02:00
60f2a73cc5 Don't check if the user is a member of the parent club if there is no parent club 2020-09-18 13:35:55 +02:00
bcd96b2ed8 The BDE membership and the club membership must now be in two parts 2020-09-18 12:35:36 +02:00
159 changed files with 12114 additions and 1454 deletions

View File

@ -10,7 +10,6 @@ DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost
DOMAIN=localhost
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost

5
.gitignore vendored
View File

@ -47,3 +47,8 @@ backups/
env/
venv/
db.sqlite3
# ansibles customs host
ansible/host_vars/*.yaml
!ansible/host_vars/bde*
ansible/hosts

View File

@ -1,6 +1,7 @@
stages:
- test
- quality-assurance
- docs
# Also fetch submodules
variables:
@ -16,8 +17,8 @@ py37-django22:
apt-get install --no-install-recommends -t buster-backports -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py37-django22
@ -33,11 +34,26 @@ py38-django22:
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py38-django22
# Debian Bullseye
py39-django22:
stage: test
image: debian:bullseye
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py39-django22
linters:
stage: quality-assurance
image: debian:buster-backports
@ -47,3 +63,17 @@ linters:
# Be nice to new contributors, but please use `tox`
allow_failure: true
# Compile documentation
documentation:
stage: docs
image: sphinxdoc/sphinx
before_script:
- pip install sphinx-rtd-theme
- cd docs
script:
- make dirhtml
artifacts:
paths:
- docs/_build
expire_in: 1 day

View File

@ -8,8 +8,8 @@ RUN apt-get update && \
apt-get install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \

View File

@ -69,13 +69,31 @@ accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu
de la note sur un téléphone !
## Installation d'une instance de production
Pour déployer facilement la note il est possible d'utiliser le playbook Ansible (sinon vous pouvez toujours le faire a la main, voir plus bas).
### Avec ansible
Il vous faudra un serveur sous debian ou ubuntu connecté à internet et que vous souhaiterez accéder à cette instance de la note sur `note.nomdedomaine.tld`.
0. Installer Ansible sur votre machine personnelle.
0. (bis) cloner le dépot sur votre machine personelle.
1. Copier le fichier `ansible/host_example`
``` bash
$ cp ansible/hosts_example ansible/hosts
```
et ajouter sous [dev] et/ou [prod] les serveurs sur lesquels vous souhaitez installer la note.
2. Créer un fichier `ansible/host_vars/<note.nomdedomaine.tld.yaml>` sur le modèle des fichiers existants dans `ansible/hosts` et compléter les variables nécessaires.
3. lancer `ansible/base.yaml -l <nomdedomaine.tld.yaml>`
4. Aller vous faire un café, ca peux durer un moment.
### Installation manuelle
**En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.**
Cela permet de mettre à jour facilement les dépendances critiques telles que Django.
L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**.
Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant.
Sinon vous pouvez suivre les étapes décrites ci-dessous.
0. Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports.
@ -93,10 +111,10 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
$ sudo apt install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools python3-docutils \
memcached uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
nginx python3-venv git acl
```
@ -261,20 +279,25 @@ Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.cr
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
**Commentez votre code !**
La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
La documentation plus haut niveau sur le développement et sur l'utilisation
est disponible sur <https://note.crans.org/doc> et également dans le dossier `docs`.
## FAQ
### Regénérer les fichiers de traduction
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`.
Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
De plus, il faut aussi extraire les variables des fichiers JavaScript.
```bash
django-admin makemessages -i env
python3 manage.py makemessages -i env
python3 manage.py makemessages -i env -e js -d djangojs
```
Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec
```bash
django-admin compilemessages
python3 manage.py compilemessages
python3 manage.py compilejsmessages
```

View File

@ -7,7 +7,7 @@
prompt: "Password of the database (leave it blank to skip database init)"
private: yes
vars:
mirror: deb.debian.org
mirror: mirror.crans.org
roles:
- 1-apt-basic
- 2-nk20
@ -16,3 +16,4 @@
- 5-nginx
- 6-psql
- 7-postinstall
- 8-docs

View File

@ -3,3 +3,4 @@ note:
server_name: note-beta.crans.org
git_branch: beta
cron_enabled: false
email: notekfet2020@lists.crans.org

View File

@ -3,3 +3,4 @@ note:
server_name: note-dev.crans.org
git_branch: beta
cron_enabled: false
email: notekfet2020@lists.crans.org

View File

@ -3,3 +3,4 @@ note:
server_name: note.crans.org
git_branch: master
cron_enabled: true
email: notekfet2020@lists.crans.org

View File

@ -1,5 +1,5 @@
[dev]
bde3-virt.adh.crans.org
bde-note-dev.adh.crans.org
bde-nk20-beta.adh.crans.org
[prod]

View File

@ -3,11 +3,12 @@
apt_repository:
repo: deb http://{{ mirror }}/debian buster-backports main
state: present
when: ansible_facts['distribution'] == "Debian"
- name: Install note_kfet APT dependencies
apt:
update_cache: true
default_release: buster-backports
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
install_recommends: false
name:
# Common tools
@ -23,13 +24,14 @@
- python3-babel
- python3-bs4
- python3-django
- python3-django-cas-server
- python3-django-crispy-forms
- python3-django-extensions
- python3-django-filters
- python3-django-oauth-toolkit
- python3-django-polymorphic
- python3-djangorestframework
- python3-lockfile
- python3-memcache
- python3-phonenumbers
- python3-pil
- python3-pip
@ -40,6 +42,9 @@
# LaTeX (PDF generation)
- texlive-xetex
# Cache server
- memcached
# WSGI server
- uwsgi
- uwsgi-plugin-python3

View File

@ -16,7 +16,7 @@
- name: Use default env vars (should be updated!)
template:
src: "env_example"
src: "env.j2"
dest: "/var/www/note_kfet/.env"
mode: 0644
force: false
@ -36,3 +36,13 @@
dest: /etc/cron.d/note
owner: root
group: root
- name: Set default directory to /var/www/note_kfet
lineinfile:
path: /etc/skel/.bashrc
line: 'cd /var/www/note_kfet'
- name: Automatically source Python virtual environment
lineinfile:
path: /etc/skel/.bashrc
line: 'source /var/www/note_kfet/env/bin/activate'

View File

@ -0,0 +1,23 @@
DJANGO_APP_STAGE=prod
# Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev
DJANGO_DEV_STORE_METHOD=sqlite
DJANGO_DB_HOST=localhost
DJANGO_DB_NAME=note_db
DJANGO_DB_USER=note
DJANGO_DB_PASSWORD={{ DB_PASSWORD }}
DJANGO_DB_PORT=
DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL= {{note.server_name}}
# Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost
EMAIL_HOST=smtp.localhost
EMAIL_PORT=25
EMAIL_USER=notekfet@localhost
EMAIL_PASSWORD=CHANGE_ME
# Wiki configuration
WIKI_USER=NoteKfet2020
WIKI_PASSWORD=

View File

@ -9,6 +9,11 @@
retries: 3
until: pkg_result is succeeded
- name: Check if certificate already exists.
stat:
path: /etc/letsencrypt/live/{{note.server_name}}/cert.pem
register: letsencrypt_cert
- name: Create /etc/letsencrypt/conf.d
file:
path: /etc/letsencrypt/conf.d
@ -19,3 +24,17 @@
src: "letsencrypt/conf.d/nk20.ini.j2"
dest: "/etc/letsencrypt/conf.d/nk20.ini"
mode: 0644
- name: Stop services to allow certbot to generate a cert.
service:
name: nginx
state: stopped
- name: Generate new certificate if one doesn't exist.
shell: "certbot certonly --non-interactive --agree-tos --config /etc/letsencrypt/conf.d/nk20.ini -d {{note.server_name}}"
when: letsencrypt_cert.stat.exists == False
- name: Restart services to allow certbot to generate a cert.
service:
name: nginx
state: started

View File

@ -10,7 +10,7 @@ rsa-key-size = 4096
# server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment and update to register with the specified e-mail address
email = notekfet2020@lists.crans.org
email = {{ note.email }}
# Uncomment to use a text interface instead of ncurses
text = True

View File

@ -50,6 +50,10 @@ server {
alias /var/www/note_kfet/static; # your Django project's static files - amend as required
}
location /doc {
alias /var/www/documentation; # The documentation of the project
}
# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass note;

View File

@ -11,14 +11,14 @@
until: pkg_result is succeeded
- name: Create role note
when: "DB_PASSWORD|bool" # If the password is not defined, skip the installation
when: DB_PASSWORD|length > 0 # If the password is not defined, skip the installation
postgresql_user:
name: note
password: "{{ DB_PASSWORD }}"
become_user: postgres
- name: Create NK20 database
when: "DB_PASSWORD|bool"
when: DB_PASSWORD|length >0
postgresql_db:
name: note_db
owner: note

View File

@ -1,4 +1,10 @@
---
- name: Collect static files
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Migrate Django database
command: /var/www/note_kfet/env/bin/python manage.py migrate
args:
@ -11,14 +17,14 @@
chdir: /var/www/note_kfet
become_user: www-data
- name: Compile JavaScript messages
command: /var/www/note_kfet/env/bin/python manage.py compilejsmessages
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Install initial fixtures
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
args:
chdir: /var/www/note_kfet
become_user: postgres
- name: Collect static files
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
args:
chdir: /var/www/note_kfet
become_user: www-data

View File

@ -0,0 +1,20 @@
---
- name: Install Sphinx and RTD theme
pip:
requirements: /var/www/note_kfet/docs/requirements.txt
virtualenv: /var/www/note_kfet/env
virtualenv_command: /usr/bin/python3 -m venv
virtualenv_site_packages: true
become_user: www-data
- name: Create documentation directory with good permissions
file:
path: /var/www/documentation
state: directory
owner: www-data
group: www-data
mode: u=rwx,g=rwxs,o=rx
- name: Build HTML documentation
command: /var/www/note_kfet/env/bin/sphinx-build -b dirhtml /var/www/note_kfet/docs/ /var/www/documentation/
become_user: www-data

View File

@ -15,10 +15,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/type/
"""
queryset = ActivityType.objects.all()
queryset = ActivityType.objects.order_by('id')
serializer_class = ActivityTypeSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'can_invite', ]
filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ]
class ActivityViewSet(ReadProtectedModelViewSet):
@ -27,10 +27,16 @@ class ActivityViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/activity/
"""
queryset = Activity.objects.all()
queryset = Activity.objects.order_by('id')
serializer_class = ActivitySerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'description', 'activity_type', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
'date_start', 'date_end', 'valid', 'open', ]
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
'$creater__email', '$creater__note__alias__name', '$creater__note__alias__normalized_name',
'$organizer__name', '$organizer__email', '$organizer__note__alias__name',
'$organizer__note__alias__normalized_name', '$attendees_club__name', '$attendees_club__email',
'$attendees_club__note__alias__name', '$attendees_club__note__alias__normalized_name', ]
class GuestViewSet(ReadProtectedModelViewSet):
@ -39,10 +45,13 @@ class GuestViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/guest/
"""
queryset = Guest.objects.all()
queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer
filter_backends = [SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ]
class EntryViewSet(ReadProtectedModelViewSet):
@ -51,7 +60,9 @@ class EntryViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/entry/
"""
queryset = Entry.objects.all()
queryset = Entry.objects.order_by('id')
serializer_class = EntrySerializer
filter_backends = [SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['activity', 'time', 'note', 'guest', ]
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
'$guest__last_name', '$guest__first_name', ]

View File

@ -30,7 +30,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
})
.done(function() {
addMsg('Invité supprimé','success');
addMsg('{% trans "Guest deleted" %}', 'success');
$("#guests_table").load(location.pathname + " #guests_table");
})
.fail(function(xhr, textStatus, error) {

View File

@ -86,10 +86,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
}).done(function () {
if (target.hasClass("table-info"))
addMsg(
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.",
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
"warning", 10000);
else
addMsg("Entrée effectuée !", "success", 4000);
addMsg("Entry made!", "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
@ -121,10 +121,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
}).done(function () {
if (target.hasClass("table-info"))
addMsg(
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.",
"{% trans "Entry done, but caution: the user is not a Kfet member." %}",
"warning", 10000);
else
addMsg("Entrée effectuée !", "success", 4000);
addMsg("{% trans "Entry done!" %}", "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);

View File

@ -3,13 +3,16 @@
from datetime import timedelta
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from activity.models import Activity, ActivityType, Guest, Entry
from member.models import Club
from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
from ..models import Activity, ActivityType, Guest, Entry
class TestActivities(TestCase):
"""
@ -173,3 +176,58 @@ class TestActivities(TestCase):
"""
response = self.client.get(reverse("activity:calendar_ics"))
self.assertEqual(response.status_code, 200)
class TestActivityAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.activity = Activity.objects.create(
name="Activity",
description="This is a test activity\non two very very long lines\nbecause this is very important.",
location="Earth",
activity_type=ActivityType.objects.get(name="Pot"),
creater=self.user,
organizer=Club.objects.get(name="Kfet"),
attendees_club=Club.objects.get(name="Kfet"),
date_start=timezone.now(),
date_end=timezone.now() + timedelta(days=2),
valid=True,
)
self.guest = Guest.objects.create(
activity=self.activity,
inviter=self.user.note,
last_name="GUEST",
first_name="Guest",
)
self.entry = Entry.objects.create(
activity=self.activity,
note=self.user.note,
guest=self.guest,
)
def test_activity_api(self):
"""
Load Activity API page and test all filters and permissions
"""
self.check_viewset(ActivityViewSet, "/api/activity/activity/")
def test_activity_type_api(self):
"""
Load ActivityType API page and test all filters and permissions
"""
self.check_viewset(ActivityTypeViewSet, "/api/activity/type/")
def test_entry_api(self):
"""
Load Entry API page and test all filters and permissions
"""
self.check_viewset(EntryViewSet, "/api/activity/entry/")
def test_guest_api(self):
"""
Load Guest API page and test all filters and permissions
"""
self.check_viewset(GuestViewSet, "/api/activity/guest/")

View File

@ -12,8 +12,10 @@ from django.db.models import F, Q
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser
@ -288,6 +290,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
return context
# Cache for 1 hour
@method_decorator(cache_page(60 * 60), name='dispatch')
class CalendarView(View):
"""
Render an ICS calendar with all valid activities.

View File

@ -4,10 +4,14 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from rest_framework.serializers import ModelSerializer
from django.utils import timezone
from rest_framework import serializers
from member.api.serializers import ProfileSerializer, MembershipSerializer
from note.api.serializers import NoteSerializer
from note.models import Alias
class UserSerializer(ModelSerializer):
class UserSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -22,7 +26,7 @@ class UserSerializer(ModelSerializer):
)
class ContentTypeSerializer(ModelSerializer):
class ContentTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -31,3 +35,42 @@ class ContentTypeSerializer(ModelSerializer):
class Meta:
model = ContentType
fields = '__all__'
class OAuthSerializer(serializers.ModelSerializer):
"""
Informations that are transmitted by OAuth.
For now, this includes user, profile and valid memberships.
This should be better managed later.
"""
normalized_name = serializers.SerializerMethodField()
profile = ProfileSerializer()
note = NoteSerializer()
memberships = serializers.SerializerMethodField()
def get_normalized_name(self, obj):
return Alias.normalize(obj.username)
def get_memberships(self, obj):
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
class Meta:
model = User
fields = (
'id',
'username',
'normalized_name',
'first_name',
'last_name',
'email',
'is_superuser',
'is_active',
'is_staff',
'profile',
'note',
'memberships',
)

240
apps/api/tests.py Normal file
View File

@ -0,0 +1,240 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from datetime import datetime, date
from decimal import Decimal
from urllib.parse import quote_plus
from warnings import warn
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.files import ImageFieldFile
from django.test import TestCase
from django_filters.rest_framework import DjangoFilterBackend
from member.models import Membership, Club
from note.models import NoteClub, NoteUser, Alias, Note
from permission.models import PermissionMask, Permission, Role
from phonenumbers import PhoneNumber
from rest_framework.filters import SearchFilter, OrderingFilter
from .viewsets import ContentTypeViewSet, UserViewSet
class TestAPI(TestCase):
"""
Load API pages and check that filters are working.
"""
fixtures = ('initial', )
def setUp(self) -> None:
self.user = User.objects.create_superuser(
username="adminapi",
password="adminapi",
email="adminapi@example.com",
last_name="Admin",
first_name="Admin",
)
self.client.force_login(self.user)
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
def check_viewset(self, viewset, url):
"""
This function should be called inside a unit test.
This loads the viewset and for each filter entry, it checks that the filter is running good.
"""
resp = self.client.get(url + "?format=json")
self.assertEqual(resp.status_code, 200)
model = viewset.serializer_class.Meta.model
if not model.objects.exists(): # pragma: no cover
warn(f"Warning: unable to test API filters for the model {model._meta.verbose_name} "
"since there is no instance of it.")
return
if hasattr(viewset, "filter_backends"):
backends = viewset.filter_backends
obj = model.objects.last()
if DjangoFilterBackend in backends:
# Specific search
for field in viewset.filterset_fields:
obj = self.fix_note_object(obj, field)
value = self.get_value(obj, field)
if value is None: # pragma: no cover
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
"has not been tested.")
continue
resp = self.client.get(url + f"?format=json&{field}={quote_plus(str(value))}")
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
content = json.loads(resp.content)
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
if OrderingFilter in backends:
# Ensure that ordering is working well
for field in viewset.ordering_fields:
resp = self.client.get(url + f"?ordering={field}")
self.assertEqual(resp.status_code, 200)
resp = self.client.get(url + f"?ordering=-{field}")
self.assertEqual(resp.status_code, 200)
if SearchFilter in backends:
# Basic search
for field in viewset.search_fields:
obj = self.fix_note_object(obj, field)
if field[0] == '$' or field[0] == '=':
field = field[1:]
value = self.get_value(obj, field)
if value is None: # pragma: no cover
warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} "
"has not been tested.")
continue
resp = self.client.get(url + f"?format=json&search={quote_plus(str(value))}")
self.assertEqual(resp.status_code, 200, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
content = json.loads(resp.content)
self.assertGreater(content["count"], 0, f"The filter {field} for the model "
f"{model._meta.verbose_name} does not work. "
f"Given parameter: {value}")
self.check_permissions(url, obj)
def check_permissions(self, url, obj):
"""
Check that permissions are working
"""
# Drop rights
self.user.is_superuser = False
self.user.save()
sess = self.client.session
sess["permission_mask"] = 0
sess.save()
# Delete user permissions
for m in Membership.objects.filter(user=self.user).all():
m.roles.clear()
m.save()
# Create a new role, which will have the checking permission
role = Role.objects.get_or_create(name="β-tester")[0]
role.permissions.clear()
role.save()
membership = Membership.objects.get_or_create(user=self.user, club=Club.objects.get(name="BDE"))[0]
membership.roles.set([role])
membership.save()
# Ensure that the access to the object is forbidden without permission
resp = self.client.get(url + f"{obj.pk}/")
self.assertEqual(resp.status_code, 404, f"Mysterious access to {url}{obj.pk}/ for {obj}")
obj.refresh_from_db()
# There are problems with polymorphism
if isinstance(obj, Note) and hasattr(obj, "note_ptr"):
obj = obj.note_ptr
mask = PermissionMask.objects.get(rank=0)
for field in obj._meta.fields:
# Build permission query
value = self.get_value(obj, field.name)
if isinstance(value, date) or isinstance(value, datetime):
value = value.isoformat()
elif isinstance(value, ImageFieldFile):
value = value.name
elif isinstance(value, Decimal):
value = str(value)
query = json.dumps({field.name: value})
# Create sample permission
permission = Permission.objects.get_or_create(
model=ContentType.objects.get_for_model(obj._meta.model),
query=query,
mask=mask,
type="view",
permanent=False,
description=f"Can view {obj._meta.verbose_name}",
)[0]
role.permissions.set([permission])
role.save()
# Check that the access is possible
resp = self.client.get(url + f"{obj.pk}/")
self.assertEqual(resp.status_code, 200, f"Permission {permission.query} is not working "
f"for the model {obj._meta.verbose_name}")
# Restore rights
self.user.is_superuser = True
self.user.save()
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
@staticmethod
def get_value(obj, key: str):
"""
Resolve the queryset filter to get the Python value of an object.
"""
if hasattr(obj, "all"):
# obj is a RelatedManager
obj = obj.last()
if obj is None: # pragma: no cover
return None
if '__' not in key:
obj = getattr(obj, key)
if hasattr(obj, "pk"):
return obj.pk
elif hasattr(obj, "all"):
if not obj.exists(): # pragma: no cover
return None
return obj.last().pk
elif isinstance(obj, bool):
return int(obj)
elif isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, PhoneNumber):
return obj.raw_input
return obj
key, remaining = key.split('__', 1)
return TestAPI.get_value(getattr(obj, key), remaining)
@staticmethod
def fix_note_object(obj, field):
"""
When querying an object that has a noteclub or a noteuser field,
ensure that the object has a good value.
"""
if isinstance(obj, Alias):
if "noteuser" in field:
return NoteUser.objects.last().alias.last()
elif "noteclub" in field:
return NoteClub.objects.last().alias.last()
elif isinstance(obj, Note):
if "noteuser" in field:
return NoteUser.objects.last()
elif "noteclub" in field:
return NoteClub.objects.last()
return obj
class TestBasicAPI(TestAPI):
def test_user_api(self):
"""
Load the user page.
"""
self.check_viewset(ContentTypeViewSet, "/api/models/")
self.check_viewset(UserViewSet, "/api/user/")

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.conf.urls import url, include
from rest_framework import routers
from .views import UserInformationView
from .viewsets import ContentTypeViewSet, UserViewSet
# Routers provide an easy way of automatically determining the URL conf.
@ -47,5 +48,6 @@ app_name = 'api'
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^me/', UserInformationView.as_view()),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

20
apps/api/views.py Normal file
View File

@ -0,0 +1,20 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from rest_framework.generics import RetrieveAPIView
from .serializers import OAuthSerializer
class UserInformationView(RetrieveAPIView):
"""
These fields are give to OAuth authenticators.
"""
serializer_class = OAuthSerializer
def get_queryset(self):
return User.objects.filter(pk=self.request.user.pk)
def get_object(self):
return self.request.user

View File

@ -6,6 +6,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
from django.conf import settings
from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_session
@ -48,12 +49,13 @@ class UserViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
then render it on /api/users/
then render it on /api/user/
"""
queryset = User.objects.all()
queryset = User.objects
serializer_class = UserSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active',
'note__alias__name', 'note__alias__normalized_name', ]
def get_queryset(self):
queryset = super().get_queryset()
@ -106,7 +108,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
then render it on /api/users/
then render it on /api/models/
"""
queryset = ContentType.objects.all()
queryset = ContentType.objects.order_by('id')
serializer_class = ContentTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ]

View File

@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
then render it on /api/logs/
"""
queryset = Changelog.objects.all()
queryset = Changelog.objects.order_by('id')
serializer_class = ChangelogSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]

View File

@ -1,7 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.filters import SearchFilter
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
@ -14,8 +15,15 @@ class ProfileViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
then render it on /api/members/profile/
"""
queryset = Profile.objects.all()
queryset = Profile.objects.order_by('id')
serializer_class = ProfileSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
'ml_art_registration', 'report_frequency', 'email_confirmed', 'registration_valid', ]
search_fields = ['$user__first_name', '$user__last_name', '$user__username', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', ]
class ClubViewSet(ReadProtectedModelViewSet):
@ -24,10 +32,13 @@ class ClubViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
then render it on /api/members/club/
"""
queryset = Club.objects.all()
queryset = Club.objects.order_by('id')
serializer_class = ClubSerializer
filter_backends = [SearchFilter]
search_fields = ['$name', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
'membership_duration', 'membership_start', 'membership_end', ]
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
class MembershipViewSet(ReadProtectedModelViewSet):
@ -36,5 +47,14 @@ class MembershipViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
then render it on /api/members/membership/
"""
queryset = Membership.objects.all()
queryset = Membership.objects.order_by('id')
serializer_class = MembershipSerializer
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
'user__username', 'user__last_name', 'user__first_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name',
'date_start', 'date_end', 'fee', 'roles', ]
ordering_fields = ['id', 'date_start', 'date_end', ]
search_fields = ['$club__name', '$club__email', '$club__note__alias__name', '$club__note__alias__normalized_name',
'$user__username', '$user__last_name', '$user__first_name', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', '$roles__name', ]

17
apps/member/auth.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
from note.models import Alias
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
"""
Override Django Auth User model to define a custom Matrix username.
"""
def attributs(self):
d = super().attributs()
if self.user:
d["normalized_name"] = Alias.normalize(self.user.username)
return d

View File

@ -150,6 +150,7 @@ class ClubForm(forms.ModelForm):
"membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete(
Club,
resetable=True,
attrs={
'api_url': '/api/members/club/',
}

View File

@ -7,6 +7,7 @@ def create_bde_and_kfet(apps, schema_editor):
"""
Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub")
Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
@ -45,6 +46,19 @@ def create_bde_and_kfet(apps, schema_editor):
polymorphic_ctype_id=polymorphic_ctype_id,
)
Alias.objects.get_or_create(
id=5,
note_id=5,
name="BDE",
normalized_name="bde",
)
Alias.objects.get_or_create(
id=6,
note_id=6,
name="Kfet",
normalized_name="kfet",
)
class Migration(migrations.Migration):
dependencies = [

View File

@ -0,0 +1,50 @@
import sys
from django.db import migrations
def give_note_account_permissions(apps, schema_editor):
"""
Automatically manage the membership of the Note account.
"""
User = apps.get_model("auth", "user")
Membership = apps.get_model("member", "membership")
Role = apps.get_model("permission", "role")
note = User.objects.filter(username="note")
if not note.exists():
# We are in a test environment, don't log error message
if len(sys.argv) > 1 and sys.argv[1] == 'test':
return
print("Warning: Note account was not found. The note account was not imported.")
print("Make sure you have imported the NK15 database. The new import script handles correctly the permissions.")
print("This migration will be ignored, you can re-run it if you forgot the note account or ignore it if you "
"don't want this account.")
return
note = note.get()
# Set for the two clubs a large expiration date and the correct role.
for m in Membership.objects.filter(user_id=note.id).all():
m.date_end = "3142-12-12"
m.roles.set(Role.objects.filter(name="PC Kfet").all())
m.save()
# By default, the note account is only authorized to be logged from localhost.
note.password = "ipbased$127.0.0.1"
note.is_active = True
note.save()
# Ensure that the note of the account is disabled
note.note.inactivity_reason = 'forced'
note.note.is_active = False
note.save()
class Migration(migrations.Migration):
dependencies = [
('member', '0005_remove_null_tag_on_charfields'),
('permission', '0001_initial'),
]
operations = [
migrations.RunPython(give_note_account_permissions),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0006_create_note_account_bde_membership'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='roles',
field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
),
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -313,6 +313,7 @@ class Membership(models.Model):
roles = models.ManyToManyField(
"permission.Role",
related_name="memberships",
verbose_name=_("roles"),
)

View File

@ -14,7 +14,7 @@ function create_alias (e) {
}).done(function () {
// Reload table
$('#alias_table').load(location.pathname + ' #alias_table')
addMsg('Alias ajouté', 'success')
addMsg(gettext('Alias successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
@ -22,7 +22,7 @@ function create_alias (e) {
/**
* On click of "delete", delete the alias
* @param Integer button_id Alias id to remove
* @param button_id:Integer Alias id to remove
*/
function delete_button (button_id) {
$.ajax({
@ -30,7 +30,7 @@ function delete_button (button_id) {
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg('Alias supprimé', 'success')
addMsg(gettext('Alias successfully deleted'), 'success')
$('#alias_table').load(location.pathname + ' #alias_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)

View File

@ -43,8 +43,24 @@ class UserTable(tables.Table):
section = tables.Column(accessor='profile__section')
# Override the column to let replace the URL
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail
# Replace also the URL
if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
value = ""
record.email = value
return value
def render_section(self, record, value):
return value \
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
else ""
def render_balance(self, record, value):
return pretty_money(value)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else ""
@ -112,7 +128,7 @@ class MembershipTable(tables.Table):
fee=0,
)
if PermissionBackend.check_perm(get_current_authenticated_user(),
"member:add_membership", empty_membership): # If the user has right
"member.add_membership", empty_membership): # If the user has right
renew_url = reverse_lazy('member:club_renew_membership',
kwargs={"pk": record.pk})
t = format_html(

View File

@ -13,15 +13,29 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if additional_fee_renewal %}
<div class="alert alert-warning">
{% if renewal %}
{% if club.name == "Kfet" %} {# Auto-renewal #}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }}
will be charged to renew automatically the membership in this/these club·s.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
The user is not a member of the club·s {{ clubs }}. Please create the required memberships,
otherwise it will fail.
{% endblocktrans %}
{% endif %}
{% else %}
{% if club.name == "Kfet" %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }}
will be charged to adhere automatically to this/these club·s.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
This club has parents {{ clubs }}. Please make sure that the user is a member of this or these club·s,
otherwise the creation of this membership will fail.
{% endblocktrans %}
{% endif %}
{% endif %}
</div>
{% endif %}

View File

@ -48,7 +48,7 @@
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
<i class="fa fa-edit"></i>
{% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }})
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
</a>
</dd>

View File

@ -21,10 +21,11 @@
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
<i class="fa fa-edit"></i>
{% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }})
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
</a>
</dd>
{% if "member.view_profile"|has_perm:user_object.profile %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
@ -45,6 +46,7 @@
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
{% endif %}
{% endif %}
</dl>
{% if user_object.pk == user.pk %}

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n perms %}
{% block content %}
{% if "member.change_profile_registration_valid"|has_perm:user %}
{% if can_manage_registrations %}
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
</a>

View File

View File

@ -0,0 +1,22 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from django import template
from django.contrib.auth.models import User
from ..models import Club, Membership
def is_member(user, club):
if isinstance(user, str):
club = User.objects.get(username=user)
if isinstance(club, str):
club = Club.objects.get(name=club)
return Membership.objects\
.filter(user=user, club=club, date_start__lte=date.today(), date_end__gte=date.today()).exists()
register = template.Library()
register.filter("is_member", is_member)

View File

@ -41,7 +41,7 @@ class TemplateLoggedInTests(TestCase):
password="adminadmin",
permission_mask=3,
))
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 200)
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
def test_logout(self):
response = self.client.get(reverse("logout"))

View File

@ -5,17 +5,20 @@ import hashlib
import os
from datetime import date, timedelta
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from member.models import Club, Membership, Profile
from note.models import Alias, NoteSpecial
from permission.models import Role
from treasury.models import SogeCredit
from ..api.views import ClubViewSet, MembershipViewSet, ProfileViewSet
from ..models import Club, Membership, Profile
"""
Create some users and clubs and test that all pages are rendering properly
and that memberships are working.
@ -205,7 +208,7 @@ class TestMemberships(TestCase):
first_name="Toto",
bank="Le matelas",
))
self.assertRedirects(response, club.get_absolute_url(), 302, 200)
self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
self.assertTrue(Membership.objects.filter(user=user, club=club).exists())
@ -244,9 +247,9 @@ class TestMemberships(TestCase):
first_name="Toto",
bank="Bank",
))
self.assertRedirects(response, club.get_absolute_url(), 302, 200)
self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
response = self.client.get(user.profile.get_absolute_url())
response = self.client.get(club.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_auto_join_kfet_when_join_bde_with_soge(self):
@ -273,7 +276,7 @@ class TestMemberships(TestCase):
first_name="Toto",
bank="Société générale",
))
self.assertRedirects(response, bde.get_absolute_url(), 302, 200)
self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
self.assertTrue(Membership.objects.filter(user=user, club=bde).exists())
self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists())
@ -403,3 +406,46 @@ class TestMemberships(TestCase):
self.user.password = "custom_nk15$1$" + salt + "|" + hashed
self.user.save()
self.assertTrue(self.user.check_password(password))
class TestMemberAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.user.profile.registration_valid = True
self.user.profile.email_confirmed = True
self.user.profile.phone_number = "0600000000"
self.user.profile.section = "1A0"
self.user.profile.department = "A0"
self.user.profile.address = "Earth"
self.user.profile.save()
self.club = Club.objects.create(
name="totoclub",
parent_club=Club.objects.get(name="BDE"),
membership_start=date(year=1970, month=1, day=1),
membership_end=date(year=2040, month=1, day=1),
membership_duration=365 * 10,
)
self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
self.membership = Membership.objects.create(user=self.user, club=self.club)
self.membership.roles.add(Role.objects.get(name="Bureau de club"))
self.membership.save()
def test_club_api(self):
"""
Load Club API page and test all filters and permissions
"""
self.check_viewset(ClubViewSet, "/api/members/club/")
def test_profile_api(self):
"""
Load Profile API page and test all filters and permissions
"""
self.check_viewset(ProfileViewSet, "/api/members/profile/")
def test_membership_api(self):
"""
Load Membership API page and test all filters and permissions
"""
self.check_viewset(MembershipViewSet, "/api/members/membership/")

View File

@ -70,6 +70,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.")
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency:
@ -157,8 +158,12 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table
club_list = Membership.objects.filter(user=user, date_end__gte=date.today())\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("club__name", "-date_start")
# Display only the most recent membership
club_list = club_list.distinct("club__name")\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
membership_table = MembershipTable(data=club_list, prefix='membership-')
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
context['club_list'] = membership_table
@ -166,6 +171,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteUser.objects.get(pk=user.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = True
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\
@ -178,6 +185,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
old_note._force_save = True
old_note._no_signal = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
@ -227,6 +235,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
.filter(profile__registration_valid=False)
context["can_manage_registrations"] = pre_registered_users.exists()
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
@ -240,8 +255,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
.filter_queryset(self.request.user, Alias, "view")).all())
context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note,
name="",
@ -392,7 +407,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates()
# managers list
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
date_start__lte=date.today(), date_end__gte=date.today())\
.order_by('user__last_name').all()
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history
@ -405,8 +421,12 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# member list
club_member = Membership.objects.filter(
club=club,
date_end__gte=date.today(),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
date_end__gte=date.today() - timedelta(days=15),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("user__username", "-date_start")
# Display only the most recent membership
club_member = club_member.distinct("user__username")\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member
membership_table = MembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
@ -438,8 +458,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
.filter_queryset(self.request.user, Alias, "view")).all())
context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note,
name="",
@ -610,6 +630,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
if not credit_type:
credit_amount = 0
if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter(
club__name="Kfet",
user=user,
@ -631,6 +654,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
form.add_error('user', _('User is already a member of the club'))
error = True
# Must join the parent club before joining this club, except for the Kfet club where it can be at the same time.
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
user=form.instance.user,
club=club.parent_club,
date_start__lte=timezone.now(),
date_end__gte=club.parent_club.membership_end,
).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
error = True
if club.membership_start and form.instance.date_start < club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
@ -645,11 +678,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
error = True
if not first_name:
form.add_error('first_name', _("This field is required."))
error = True
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
return self.form_invalid(form)
error = True
return not error
@ -663,6 +698,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
.get(pk=self.kwargs["club_pk"])
user = form.instance.user
old_membership = None
else: # get from url for renewal
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
@ -710,6 +746,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# When we renew the BDE membership, we update the profile section
# that should happens at least once a year.
user.profile.section = user.profile.section_generated
user.profile._force_save = True
user.profile.save()
# Credit note before the membership is created.
@ -737,6 +774,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
# Set the same roles as before
if old_membership:
member_role = member_role.union(old_membership.roles.all())
form.instance.roles.set(member_role)
form.instance._force_save = True
form.instance.save()
@ -774,7 +814,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
return ret
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):

View File

@ -15,29 +15,37 @@ from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, Alias
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
class NotePolymorphicViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
The djangorestframework plugin will get all `Note` objects (with polymorhism),
serialize it to JSON with the given serializer,
then render it on /api/note/note/
"""
queryset = Note.objects.all()
queryset = Note.objects.order_by('id')
serializer_class = NotePolymorphicSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['polymorphic_ctype', 'is_active', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
ordering_fields = ['alias__name', 'alias__normalized_name']
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
'$noteuser__user__email', '$noteclub__club__email', ]
ordering_fields = ['alias__name', 'alias__normalized_name', 'balance', 'created_at', ]
def get_queryset(self):
"""
Parse query and apply filters.
:return: The filtered set of requested notes
"""
queryset = super().get_queryset().distinct()
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
| PermissionBackend.filter_queryset(user, NoteUser, "view")
| PermissionBackend.filter_queryset(user, NoteClub, "view")
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
@ -55,18 +63,18 @@ class AliasViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/aliases/
"""
queryset = Alias.objects.all()
queryset = Alias.objects
serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note']
ordering_fields = ['name', 'normalized_name']
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ]
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# alias owner cannot be change once establish
setattr(serializer_class.Meta, 'read_only_fields', ('note',))
serializer_class.Meta.read_only_fields = ('note',)
return serializer_class
def destroy(self, request, *args, **kwargs):
@ -74,7 +82,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST)
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
def get_queryset(self):
@ -106,12 +114,12 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects.all()
queryset = Alias.objects
serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['note']
ordering_fields = ['name', 'normalized_name']
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ]
def get_queryset(self):
"""
@ -157,10 +165,11 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/category/
"""
queryset = TemplateCategory.objects.order_by("name").all()
queryset = TemplateCategory.objects.order_by('name')
serializer_class = TemplateCategorySerializer
filter_backends = [SearchFilter]
search_fields = ['$name', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'templates', 'templates__name']
search_fields = ['$name', '$templates__name', ]
class TransactionTemplateViewSet(viewsets.ModelViewSet):
@ -169,11 +178,12 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/template/
"""
queryset = TransactionTemplate.objects.order_by("name").all()
queryset = TransactionTemplate.objects.order_by('name')
serializer_class = TransactionTemplateSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
filterset_fields = ['name', 'amount', 'display', 'category', ]
search_fields = ['$name', ]
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
search_fields = ['$name', '$category__name', ]
ordering_fields = ['amount', ]
class TransactionViewSet(ReadProtectedModelViewSet):
@ -182,13 +192,17 @@ class TransactionViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/transaction/
"""
queryset = Transaction.objects.order_by("-created_at").all()
queryset = Transaction.objects.order_by('-created_at')
serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity",
"polymorphic_ctype", "amount", "created_at", ]
search_fields = ['$reason', ]
ordering_fields = ['created_at', 'amount']
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
'destination', 'destination_alias', 'destination__alias__name',
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
'created_at', 'valid', 'invalidity_reason', ]
search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name',
'$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name',
'$invalidity_reason', ]
ordering_fields = ['created_at', 'amount', ]
def get_queryset(self):
user = self.request.user

View File

@ -3,7 +3,7 @@
from django.apps import AppConfig
from django.conf import settings
from django.db.models.signals import post_save, pre_delete
from django.db.models.signals import pre_delete, pre_save, post_save
from django.utils.translation import gettext_lazy as _
from . import signals
@ -17,6 +17,15 @@ class NoteConfig(AppConfig):
"""
Define app internal signals to interact with other apps
"""
pre_save.connect(
signals.pre_save_note,
sender="note.noteuser",
)
pre_save.connect(
signals.pre_save_note,
sender="note.noteclub",
)
post_save.connect(
signals.save_user_note,
sender=settings.AUTH_USER_MODEL,

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0004_remove_null_tag_on_charfields'),
]
operations = [
migrations.AlterField(
model_name='alias',
name='note',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
),
]

View File

@ -159,20 +159,6 @@ class NoteUser(Note):
def pretty(self):
return _("%(user)s's note") % {'user': str(self.user)}
@transaction.atomic
def save(self, *args, **kwargs):
if self.pk and self.balance < 0:
old_note = NoteUser.objects.get(pk=self.pk)
super().save(*args, **kwargs)
if old_note.balance >= 0:
# Passage en négatif
self.last_negative = timezone.now()
self._force_save = True
self.save(*args, **kwargs)
self.send_mail_negative_balance()
else:
super().save(*args, **kwargs)
def send_mail_negative_balance(self):
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
@ -201,20 +187,6 @@ class NoteClub(Note):
def pretty(self):
return _("Note of %(club)s club") % {'club': str(self.club)}
@transaction.atomic
def save(self, *args, **kwargs):
if self.pk and self.balance < 0:
old_note = NoteClub.objects.get(pk=self.pk)
super().save(*args, **kwargs)
if old_note.balance >= 0:
# Passage en négatif
self.last_negative = timezone.now()
self._force_save = True
self.save(*args, **kwargs)
self.send_mail_negative_balance()
else:
super().save(*args, **kwargs)
def send_mail_negative_balance(self):
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
@ -276,6 +248,7 @@ class Alias(models.Model):
note = models.ForeignKey(
Note,
on_delete=models.PROTECT,
related_name="alias",
)
class Meta:

View File

@ -217,10 +217,14 @@ class Transaction(PolymorphicModel):
# When source == destination, no money is transferred and no transaction is created
return
self.source = Note.objects.select_for_update().get(pk=self.source_id)
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
# Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate()
if not self.source.is_active or not self.destination.is_active:
if not (hasattr(self, '_force_save') and self._force_save) \
and (not self.source.is_active or not self.destination.is_active):
raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active."))
@ -268,7 +272,7 @@ class RecurrentTransaction(Transaction):
)
def clean(self):
if self.template.destination != self.destination:
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
raise ValidationError(
_("The destination of this transaction must equal to the destination of the template."))
return super().clean()

View File

@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone
def save_user_note(instance, raw, **_kwargs):
"""
@ -25,10 +27,21 @@ def save_club_note(instance, raw, **_kwargs):
instance.note.save()
def pre_save_note(instance, raw, **_kwargs):
if not raw and instance.pk and not hasattr(instance, "_no_signal") and instance.balance < 0:
from note.models import Note
old_note = Note.objects.get(pk=instance.pk)
if old_note.balance >= 0:
# Passage en négatif
instance.last_negative = timezone.now()
instance.send_mail_negative_balance()
def delete_transaction(instance, **_kwargs):
"""
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.
"""
if not hasattr(instance, "_no_signal"):
instance.valid = False
instance._force_save = True
instance.save()

View File

@ -28,7 +28,7 @@ $(document).ready(function () {
// Switching in double consumptions mode should update the layout
$('#double_conso').change(function () {
$('#consos_list_div').removeClass('d-none')
document.getElementById('consos_list_div').classList.remove('d-none')
$('#infos_div').attr('class', 'col-sm-5 col-xl-6')
const note_list_obj = $('#note_list')
@ -37,7 +37,7 @@ $(document).ready(function () {
note_list_obj.html('')
buttons.forEach(function (button) {
$('#conso_button_' + button.id).click(function () {
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
if (LOCK) { return }
removeNote(button, 'conso_button', buttons, 'consos_list')()
})
@ -46,7 +46,7 @@ $(document).ready(function () {
})
$('#single_conso').change(function () {
$('#consos_list_div').addClass('d-none')
document.getElementById('consos_list_div').classList.add('d-none')
$('#infos_div').attr('class', 'col-sm-5 col-md-4')
const consos_list_obj = $('#consos_list')
@ -68,9 +68,9 @@ $(document).ready(function () {
})
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
$("label[for='double_conso']").removeClass('active')
document.querySelector("label[for='double_conso']").classList.remove('active')
$('#consume_all').click(consumeAll)
document.getElementById("consume_all").addEventListener('click', consumeAll)
})
notes = []
@ -127,11 +127,10 @@ function addConso (dest, amount, type, category_id, category_name, template_id,
html += li('conso_button_' + button.id, button.name +
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
})
document.getElementById(list).innerHTML = html
$('#' + list).html(html)
buttons.forEach(function (button) {
$('#conso_button_' + button.id).click(function () {
buttons.forEach((button) => {
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
if (LOCK) { return }
removeNote(button, 'conso_button', buttons, list)()
})
@ -146,12 +145,13 @@ function reset () {
notes_display.length = 0
notes.length = 0
buttons.length = 0
$('#note_list').html('')
$('#consos_list').html('')
$('#note').val('')
$('#note').attr('data-original-title', '').tooltip('hide')
$('#profile_pic').attr('src', '/static/member/img/default_picture.png')
$('#profile_pic_link').attr('href', '#')
document.getElementById('note_list').innerHTML = ''
document.getElementById('consos_list').innerHTML = ''
document.getElementById('note').value = ''
document.getElementById('note').dataset.originTitle = ''
$('#note').tooltip('hide')
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png'
document.getElementById('profile_pic_link').href = '#'
refreshHistory()
refreshBalance()
LOCK = false
@ -168,7 +168,7 @@ function consumeAll () {
let error = false
if (notes_display.length === 0) {
$('#note').addClass('is-invalid')
document.getElementById('note').classList.add('is-invalid')
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
error = true
}
@ -222,16 +222,14 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
if (!isNaN(source.balance)) {
const newBalance = source.balance - quantity * amount
if (newBalance <= -5000) {
addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' +
'succès, mais la note émettrice ' + source_alias + ' est en négatif sévère.',
'danger', 30000)
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
} else if (newBalance < 0) {
addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' +
'succès, mais la note émettrice ' + source_alias + ' est en négatif.',
'warning', 30000)
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000)
}
if (source.membership && source.membership.date_end < new Date().toISOString()) {
addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.",
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]),
'danger', 30000)
}
}
@ -253,7 +251,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
template: template
}).done(function () {
reset()
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", 'danger', 10000)
addMsg(gettext("The transaction couldn't be validated because of insufficient balance."), 'danger', 10000)
}).fail(function () {
reset()
errMsg(e.responseJSON)

View File

@ -67,7 +67,11 @@ $(document).ready(function () {
last.quantity = 1
if (!last.note.user) {
if (last.note.club) {
$('#last_name').val(last.note.name)
$('#first_name').val(last.note.name)
}
else if (!last.note.user) {
$.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) {
last.note.user = note.user
$.getJSON('/api/user/' + last.note.user + '/', function (user) {
@ -235,20 +239,20 @@ $('#btn_transfer').click(function () {
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>')
$('#amount-required').html('<strong>' + gettext('This field is required and must contain a decimal positive number.') + '</strong>')
error = true
}
const amount = Math.floor(100 * amount_field.val())
if (amount > 2147483647) {
amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>')
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
error = true
}
if (!reason_field.val()) {
if (!reason_field.val() && $('#type_transfer').is(':checked')) {
reason_field.addClass('is-invalid')
$('#reason-required').html('<strong>Ce champ est requis.</strong>')
$('#reason-required').html('<strong>' + gettext('This field is required.') + '</strong>')
error = true
}
@ -274,9 +278,8 @@ $('#btn_transfer').click(function () {
[...sources_notes_display].forEach(function (source) {
[...dests_notes_display].forEach(function (dest) {
if (source.note.id === dest.note.id) {
addMsg('Attention : la transaction de ' + pretty_money(amount) + ' de la note ' + source.name +
' vers la note ' + dest.name + " n'a pas été faite car il s'agit de la même note au départ" +
" et à l'arrivée.", 'warning', 10000)
addMsg(interpolate(gettext('Warning: the transaction of %s from %s to %s was not made because ' +
'it is the same source and destination note.'), [pretty_money(amount), source.name, dest.name]), 'warning', 10000)
LOCK = false
return
}
@ -296,43 +299,35 @@ $('#btn_transfer').click(function () {
destination_alias: dest.name
}).done(function () {
if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) {
addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.",
'danger', 30000)
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000)
}
if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) {
addMsg('Attention : la note destination ' + dest.name + " n'est plus adhérente.",
'danger', 30000)
addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000)
}
if (!isNaN(source.note.balance)) {
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
if (newBalance <= -5000) {
addMsg('Le transfert de ' +
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' +
source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' +
'mais la note émettrice est en négatif sévère.', 'danger', 10000)
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
reset()
return
} else if (newBalance < 0) {
addMsg('Le transfert de ' +
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' +
source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' +
'mais la note émettrice est en négatif.', 'warning', 10000)
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is negative.'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
reset()
return
}
}
addMsg('Le transfert de ' +
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
' vers la note ' + dest.name + ' a été fait avec succès !', 'success', 10000)
addMsg(interpolate(gettext('Transfer of %s from %s to %s succeed!'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name]), 'success', 10000)
reset()
}).fail(function (err) { // do it again but valid = false
const errObj = JSON.parse(err.responseText)
if (errObj.non_field_errors) {
addMsg('Le transfert de ' +
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
' vers la note ' + dest.name + ' a échoué : ' + errObj.non_field_errors, 'danger')
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, errObj.non_field_errors]), 'danger')
LOCK = false
return
}
@ -352,17 +347,15 @@ $('#btn_transfer').click(function () {
destination: dest.note.id,
destination_alias: dest.name
}).done(function () {
addMsg('Le transfert de ' +
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
' vers la note ' + dest.name + ' a échoué : Solde insuffisant', 'danger', 10000)
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, gettext('insufficient funds')]), 'danger', 10000)
reset()
}).fail(function (err) {
const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText }
addMsg('Le transfert de ' +
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name +
' vers la note ' + dest.name + ' a échoué : ' + error, 'danger')
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger')
LOCK = false
})
})
@ -388,7 +381,7 @@ $('#btn_transfer').click(function () {
alias = sources_notes_display[0].name
source_id = user_note.id
dest_id = special_note
reason = 'Retrait ' + $('#credit_type option:selected').text().toLowerCase()
reason = 'Retrait ' + $('#debit_type option:selected').text().toLowerCase()
if (given_reason.length > 0) { reason += ' (' + given_reason + ')' }
}
$.post('/api/note/transaction/transaction/',
@ -408,14 +401,14 @@ $('#btn_transfer').click(function () {
first_name: $('#first_name').val(),
bank: $('#bank').val()
}).done(function () {
addMsg('Le crédit/retrait a bien été effectué !', 'success', 10000)
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg('Attention : la note ' + alias + " n'est plus adhérente.", 'danger', 10000) }
addMsg(gettext('Credit/debit succeed!'), 'success', 10000)
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) }
reset()
}).fail(function (err) {
const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText }
addMsg('Le crédit/retrait a échoué : ' + error, 'danger', 10000)
addMsg(interpolate(gettext('Credit/debit failed: %s'), [error]), 'danger', 10000)
LOCK = false
})
}

View File

@ -1,15 +1,20 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from member.models import Club, Membership
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.urls import reverse
from member.models import Club, Membership
from note.models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias
from django.utils import timezone
from permission.models import Role
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
TransactionTemplateViewSet, TransactionViewSet
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
class TestTransactions(TestCase):
fixtures = ('initial', )
@ -297,8 +302,8 @@ class TestTransactions(TestCase):
def test_render_search_transactions(self):
response = self.client.get(reverse("note:transactions", args=(self.user.note.pk,)), data=dict(
source=self.second_user.note.alias_set.first().id,
destination=self.user.note.alias_set.first().id,
source=self.second_user.note.alias.first().id,
destination=self.user.note.alias.first().id,
type=[ContentType.objects.get_for_model(Transaction).id],
reason="test",
valid=True,
@ -363,3 +368,69 @@ class TestTransactions(TestCase):
self.assertTrue(Alias.objects.filter(name="test_updated_alias").exists())
response = self.client.delete("/api/note/alias/" + str(alias.pk) + "/")
self.assertEqual(response.status_code, 204)
class TestNoteAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
membership = Membership.objects.create(club=Club.objects.get(name="BDE"), user=self.user)
membership.roles.add(Role.objects.get(name="Respo info"))
membership.save()
Membership.objects.create(club=Club.objects.get(name="Kfet"), user=self.user)
self.user.note.last_negative = timezone.now()
self.user.note.save()
self.transaction = Transaction.objects.create(
source=Note.objects.first(),
destination=self.user.note,
amount=4200,
reason="Test transaction",
)
self.user.note.refresh_from_db()
Alias.objects.create(note=self.user.note, name="I am a ¢omplex alias")
self.category = TemplateCategory.objects.create(name="Test")
self.template = TransactionTemplate.objects.create(
name="Test",
destination=Club.objects.get(name="BDE").note,
category=self.category,
amount=100,
description="Test template",
)
def test_alias_api(self):
"""
Load Alias API page and test all filters and permissions
"""
self.check_viewset(AliasViewSet, "/api/note/alias/")
def test_consumer_api(self):
"""
Load Consumer API page and test all filters and permissions
"""
self.check_viewset(ConsumerViewSet, "/api/note/consumer/")
def test_note_api(self):
"""
Load Note API page and test all filters and permissions
"""
self.check_viewset(NotePolymorphicViewSet, "/api/note/note/")
def test_template_category_api(self):
"""
Load TemplateCategory API page and test all filters and permissions
"""
self.check_viewset(TemplateCategoryViewSet, "/api/note/transaction/category/")
def test_transaction_template_api(self):
"""
Load TemplateTemplate API page and test all filters and permissions
"""
self.check_viewset(TransactionTemplateViewSet, "/api/note/transaction/template/")
def test_transaction_api(self):
"""
Load Transaction API page and test all filters and permissions
"""
self.check_viewset(TransactionViewSet, "/api/note/transaction/transaction/")

View File

@ -1,8 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from .serializers import PermissionSerializer, RoleSerializer
from ..models import Permission, Role
@ -14,10 +15,11 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer,
then render it on /api/permission/permission/
"""
queryset = Permission.objects.all()
queryset = Permission.objects.order_by('id')
serializer_class = PermissionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['model', 'type', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
search_fields = ['$model__name', '$query', '$description', ]
class RoleViewSet(ReadOnlyProtectedModelViewSet):
@ -26,7 +28,8 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
then render it on /api/permission/roles/
"""
queryset = Role.objects.all()
queryset = Role.objects.order_by('id')
serializer_class = RoleSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['role', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
search_fields = ['$name', '$for_club__name', ]

View File

@ -134,8 +134,6 @@ class PermissionBackend(ModelBackend):
return False
sess = get_current_session()
if sess is not None and sess.session_key is None:
return False
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
return True

View File

@ -1,6 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import sys
from functools import lru_cache
from time import time
@ -38,6 +38,10 @@ def memoize(f):
nonlocal last_collect
if "test" in sys.argv:
# In a test environment, don't memoize permissions
return f(*args, **kwargs)
if time() - last_collect > 60:
# Clear cache
collect()

View File

@ -115,7 +115,7 @@
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"permanent": false,
"description": "Voir les aliases des notes des clubs et des adhérents du club Kfet"
}
},
@ -799,12 +799,12 @@
"member",
"membership"
],
"query": "{\"club\": [\"club\"]}",
"query": "{}",
"type": "change",
"mask": 3,
"field": "roles",
"permanent": false,
"description": "Modifier les rôles d'un adhérent d'un club"
"description": "Modifier les rôles d'une adhésion"
}
},
{
@ -819,7 +819,7 @@
"type": "change",
"mask": 1,
"field": "",
"permanent": false,
"permanent": true,
"description": "Modifier son profil"
}
},
@ -2081,7 +2081,7 @@
],
"query": "{}",
"type": "change",
"mask": 1,
"mask": 2,
"field": "invalidity_reason",
"permanent": false,
"description": "Modifier la raison d'invalidité d'une transaction"
@ -2775,6 +2775,102 @@
"description": "Modifier n'importe quel profil non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 178,
"fields": {
"model": [
"note",
"alias"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tous les alias, y compris ceux des non adhérents"
}
},
{
"model": "permission.permission",
"pk": 179,
"fields": {
"model": [
"note",
"alias"
],
"query": "{\"note__noteuser__user\": [\"user\"]}",
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"description": "Voir ses propres alias, pour toujours"
}
},
{
"model": "permission.permission",
"pk": 180,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"profile__registration_valid\": false}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel utilisateur non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 181,
"fields": {
"model": [
"member",
"profile"
],
"query": "{\"registration_valid\": false}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel profil non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 182,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club__name\": \"BDE\", \"memberships__roles__name\": \"Adhérent BDE\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel utilisateur qui est adhérent BDE"
}
},
{
"model": "permission.permission",
"pk": 183,
"fields": {
"model": [
"note",
"note"
],
"query": "{}",
"type": "change",
"mask": 1,
"field": "display_image",
"permanent": false,
"description": "Changer l'image de n'importe quelle note"
}
},
{
"model": "permission.role",
"pk": 1,
@ -2845,7 +2941,8 @@
157,
158,
159,
160
160,
179
]
}
},
@ -2906,14 +3003,14 @@
62,
127,
133,
135,
136,
141,
142,
150,
166,
167,
168
168,
182
]
}
},
@ -2927,7 +3024,9 @@
24,
25,
26,
27
27,
30,
33
]
}
},
@ -2949,6 +3048,7 @@
31,
32,
33,
51,
53,
54,
55,
@ -2972,6 +3072,7 @@
137,
138,
139,
140,
143,
146,
147,
@ -2986,7 +3087,9 @@
174,
175,
176,
177
177,
178,
183
]
}
},
@ -3168,7 +3271,13 @@
174,
175,
176,
177
177,
178,
179,
180,
181,
182,
183
]
}
},
@ -3202,7 +3311,12 @@
170,
171,
176,
177
177,
178,
179,
180,
181,
182
]
}
},
@ -3365,7 +3479,6 @@
135,
136,
137,
138,
139,
140,
143,
@ -3378,6 +3491,41 @@
]
}
},
{
"model": "permission.role",
"pk": 20,
"fields": {
"for_club": 2,
"name": "PC Kfet",
"permissions": [
6,
22,
24,
25,
26,
27,
30,
49,
50,
55,
56,
57,
58,
135,
137,
143,
147,
150,
166,
167,
168,
176,
177,
180,
181
]
}
},
{
"model": "wei.weirole",
"pk": 12,

View File

@ -43,7 +43,9 @@ class InstancedPermission:
obj = copy(obj)
obj.pk = 0
with transaction.atomic():
sid = transaction.savepoint()
for o in self.model.model_class().objects.filter(pk=0).all():
o._no_signal = True
o._force_delete = True
Model.delete(o)
# An object with pk 0 wouldn't deleted. That's not normal, we alert admins.
@ -61,9 +63,7 @@ class InstancedPermission:
obj._no_signal = True
Model.save(obj, force_insert=True)
ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists()
# Delete testing object
obj._force_delete = True
Model.delete(obj)
transaction.savepoint_rollback(sid)
return ret

View File

@ -5,7 +5,6 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter
from django import template
from note.models import Transaction
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend
@ -25,21 +24,6 @@ def not_empty_model_list(model_name):
return qs.exists()
@stringfilter
def not_empty_model_change_list(model_name):
"""
Return True if and only if the current user has right to change any object of the given model.
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
qs = model_list(model_name, "change")
return qs.exists()
@stringfilter
def model_list(model_name, t="view", fetch=True):
"""
@ -68,33 +52,8 @@ def has_perm(perm, obj):
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
def can_create_transaction():
"""
:return: True iff the authenticated user can create a transaction.
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None or isinstance(user, AnonymousUser):
return False
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
return True
if session.get("can_create_transaction", None):
return session.get("can_create_transaction", None) == 1
empty_transaction = Transaction(
source=user.note,
destination=user.note,
quantity=1,
amount=0,
reason="Check permissions",
)
session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction)
return session.get("can_create_transaction") == 1
register = template.Library()
register.filter('not_empty_model_list', not_empty_model_list)
register.filter('not_empty_model_change_list', not_empty_model_change_list)
register.filter('model_list', model_list)
register.filter('model_list_length', model_list_length)
register.filter('has_perm', has_perm)

View File

@ -78,7 +78,7 @@ class PermissionQueryTestCase(TestCase):
query = instanced.query
model = perm.model.model_class()
model.objects.filter(query).all()
except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError):
except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError): # pragma: no cover
print("Query error for permission", perm)
print("Query:", perm.query)
if instanced.query:

View File

@ -51,8 +51,10 @@ class ProtectQuerysetMixin:
# No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
# a custom request.
# We could also delete the field, but some views might be affected.
meta = form.instance._meta
for key in form.base_fields:
if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object):
if not PermissionBackend.check_perm(self.request.user,
f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
form.fields[key].widget = HiddenInput()
return form
@ -83,7 +85,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
If not, a 403 error is displayed.
"""
def get_sample_object(self):
def get_sample_object(self): # pragma: no cover
"""
return a sample instance of the Model.
It should be valid (can be stored properly in database), but must not collide with existing data.

View File

@ -44,6 +44,15 @@ class SignUpForm(UserCreationForm):
fields = ('first_name', 'last_name', 'username', 'email', )
class DeclareSogeAccountOpenedForm(forms.Form):
soge_account = forms.BooleanField(
label=_("I declare that I opened a bank account in the Société générale with the BDE partnership."),
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
"account, you will have to pay the BDE membership."),
required=False,
)
class WEISignupForm(forms.Form):
wei_registration = forms.BooleanField(
label=_("Register to the WEI"),
@ -92,14 +101,14 @@ class ValidationForm(forms.Form):
required=False,
)
join_BDE = forms.BooleanField(
join_bde = forms.BooleanField(
label=_("Join BDE Club"),
required=False,
initial=True,
)
# The user can join the Kfet club at the inscription
join_Kfet = forms.BooleanField(
join_kfet = forms.BooleanField(
label=_("Join Kfet Club"),
required=False,
initial=True,

View File

@ -3,6 +3,7 @@
import django_tables2 as tables
from django.contrib.auth.models import User
from treasury.models import SogeCredit
class FutureUserTable(tables.Table):
@ -21,6 +22,7 @@ class FutureUserTable(tables.Table):
fields = ('last_name', 'first_name', 'username', 'email', )
model = User
row_attrs = {
'class': 'table-row',
'class': lambda record: 'table-row'
+ (' bg-warning' if SogeCredit.objects.filter(user=record).exists() else ''),
'data-href': lambda record: record.pk
}

View File

@ -56,6 +56,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header text-center" >
<h4> {% trans "Validate account" %}</h4>
</div>
{% if declare_soge_account %}
<div class="alert alert-info">
{% trans "The user declared that he/she opened a bank account in the Société générale." %}
</div>
{% endif %}
<div class="card-body" id="profile_infos">
{% csrf_token %}
{{ form|crispy }}
@ -93,18 +100,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
bank.attr('disabled', true);
bank.val('Société générale');
let join_BDE = $("#id_join_BDE");
join_BDE.attr('disabled', true);
join_BDE.attr('checked', 'checked');
let join_bde = $("#id_join_bde");
let join_Kfet = $("#id_join_Kfet");
join_Kfet.attr('disabled', true);
join_Kfet.attr('checked', 'checked');
join_bde.attr('disabled', true);
join_bde.attr('checked', 'checked');
let join_kfet = $("#id_join_kfet");
join_kfet.attr('disabled', true);
join_kfet.attr('checked', 'checked');
}
soge_field.change(fillFields);
{% if object.profile.soge %}
{% if declare_soge_account %}
soge_field.attr('checked', true);
fillFields();
{% endif %}

View File

@ -196,8 +196,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_BDE=False,
join_Kfet=False,
join_bde=False,
join_kfet=False,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -210,8 +210,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_BDE=False,
join_Kfet=True,
join_bde=False,
join_kfet=True,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -224,8 +224,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="J'ai pas d'argent",
join_BDE=True,
join_Kfet=True,
join_bde=True,
join_kfet=True,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -238,8 +238,8 @@ class TestValidateRegistration(TestCase):
last_name="",
first_name="",
bank="",
join_BDE=True,
join_Kfet=True,
join_bde=True,
join_kfet=True,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -255,8 +255,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_BDE=True,
join_Kfet=False,
join_bde=True,
join_kfet=False,
))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["form"].errors)
@ -281,8 +281,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_BDE=True,
join_Kfet=False,
join_bde=True,
join_kfet=False,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()
@ -317,8 +317,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_BDE=True,
join_Kfet=True,
join_bde=True,
join_kfet=True,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()
@ -353,8 +353,8 @@ class TestValidateRegistration(TestCase):
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_BDE=True,
join_Kfet=True,
join_bde=True,
join_kfet=True,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()

View File

@ -9,6 +9,7 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
"""
Create a unique token generator to confirm email addresses.
"""
def _make_hash_value(self, user, timestamp):
"""
Hash the user's primary key and some user state that's sure to change
@ -23,9 +24,18 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
"""
# Truncate microseconds so that tokens are consistent even if the
# database doesn't support microseconds.
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
return str(user.pk) + str(user.email) + str(user.profile.email_confirmed)\
+ str(login_timestamp) + str(timestamp)
login_timestamp = (
""
if user.last_login is None
else user.last_login.replace(microsecond=0, tzinfo=None)
)
return (
str(user.pk)
+ str(user.email)
+ str(user.profile.email_confirmed)
+ str(login_timestamp)
+ str(timestamp)
)
email_validation_token = AccountActivationTokenGenerator()

View File

@ -24,7 +24,7 @@ from permission.models import Role
from permission.views import ProtectQuerysetMixin
from treasury.models import SogeCredit
from .forms import SignUpForm, ValidationForm
from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
from .tables import FutureUserTable
from .tokens import email_validation_token
@ -42,6 +42,7 @@ class UserCreateView(CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None)
del context["profile_form"].fields["section"]
del context["profile_form"].fields["report_frequency"]
del context["profile_form"].fields["last_report"]
@ -72,6 +73,13 @@ class UserCreateView(CreateView):
user.profile.send_email_validation_link()
soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
# If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
soge_credit = SogeCredit(user=user)
soge_credit._force_save = True
soge_credit.save()
return super().form_valid(form)
def get_success_url(self):
@ -182,7 +190,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
| Q(username__iregex="^" + pattern)
)
return qs[:20]
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -227,6 +235,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += 8000
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
return ctx
def get_form(self, form_class=None):
@ -251,26 +261,26 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
join_BDE = form.cleaned_data["join_BDE"]
join_Kfet = form.cleaned_data["join_Kfet"]
join_bde = form.cleaned_data["join_bde"]
join_kfet = form.cleaned_data["join_kfet"]
if soge:
# If Société Générale pays the inscription, the user joins the two clubs
join_BDE = True
join_Kfet = True
join_bde = True
join_kfet = True
if not join_BDE:
form.add_error('join_BDE', _("You must join the BDE."))
if not join_bde:
form.add_error('join_bde', _("You must join the BDE."))
return super().form_invalid(form)
fee = 0
bde = Club.objects.get(name="BDE")
bde_fee = bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
if join_BDE:
if join_bde:
fee += bde_fee
kfet = Club.objects.get(name="Kfet")
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
if join_Kfet:
if join_kfet:
fee += kfet_fee
if soge:
@ -307,6 +317,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
user.profile.save()
user.refresh_from_db()
if not soge and SogeCredit.objects.filter(user=user).exists():
# If the user declared that a bank account was opened but in the validation form the SoGé case was
# unchecked, delete the associated credit
soge_credit = SogeCredit.objects.get(user=user)
soge_credit._force_delete = True
soge_credit.delete()
if credit_type is not None and credit_amount > 0:
# Credit the note
SpecialTransaction.objects.create(
@ -321,7 +338,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
valid=True,
)
if join_BDE:
if join_bde:
# Create membership for the user to the BDE starting today
membership = Membership(
club=bde,
@ -335,7 +352,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
membership.save()
if join_Kfet:
if join_kfet:
# Create membership for the user to the Kfet starting today
membership = Membership(
club=kfet,
@ -373,6 +390,8 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
user = User.objects.filter(profile__registration_valid=False)\
.filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\
.get(pk=self.kwargs["pk"])
# Delete associated soge credits before
SogeCredit.objects.filter(user=user).delete()
user.delete()

View File

@ -16,10 +16,11 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/invoice/
"""
queryset = Invoice.objects.order_by("id").all()
queryset = Invoice.objects.order_by('id')
serializer_class = InvoiceSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['bde', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
search_fields = ['$object', '$description', '$name', '$address', ]
class ProductViewSet(ReadProtectedModelViewSet):
@ -28,10 +29,11 @@ class ProductViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/product/
"""
queryset = Product.objects.order_by("invoice_id", "id").all()
queryset = Product.objects.order_by('invoice_id', 'id')
serializer_class = ProductSerializer
filter_backends = [SearchFilter]
search_fields = ['$designation', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
search_fields = ['$designation', '$invoice__object', ]
class RemittanceTypeViewSet(ReadProtectedModelViewSet):
@ -40,8 +42,11 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer
then render it on /api/treasury/remittance_type/
"""
queryset = RemittanceType.objects.order_by("id")
queryset = RemittanceType.objects.order_by('id')
serializer_class = RemittanceTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['note', ]
search_fields = ['$note__special_type', ]
class RemittanceViewSet(ReadProtectedModelViewSet):
@ -50,8 +55,11 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/remittance/
"""
queryset = Remittance.objects.order_by("id")
queryset = Remittance.objects.order_by('id')
serializer_class = RemittanceSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
search_fields = ['$remittance_type__note__special_type', '$comment', ]
class SogeCreditViewSet(ReadProtectedModelViewSet):
@ -60,5 +68,10 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer,
then render it on /api/treasury/soge_credit/
"""
queryset = SogeCredit.objects.order_by("id")
queryset = SogeCredit.objects.order_by('id')
serializer_class = SogeCreditSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',
'$user__note__alias__normalized_name', ]

View File

@ -28,6 +28,8 @@ class TreasuryConfig(AppConfig):
source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy=None,
):
SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None)
proxy = SpecialTransactionProxy(transaction=transaction, remittance=None)
proxy._force_save = True
proxy.save()
post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy)

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.19 on 2021-03-21 09:34
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('treasury', '0002_invoice_remove_png_extension'),
]
operations = [
migrations.AlterField(
model_name='product',
name='quantity',
field=models.DecimalField(decimal_places=2, max_digits=7, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
),
migrations.AlterField(
model_name='specialtransactionproxy',
name='remittance',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='transaction_proxies', to='treasury.Remittance', verbose_name='Remittance'),
),
]

View File

@ -5,12 +5,13 @@ from datetime import date
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
class Invoice(models.Model):
@ -131,12 +132,15 @@ class Product(models.Model):
verbose_name=_("Designation"),
)
quantity = models.PositiveIntegerField(
verbose_name=_("Quantity")
quantity = models.DecimalField(
decimal_places=2,
max_digits=7,
verbose_name=_("Quantity"),
validators=[MinValueValidator(0)],
)
amount = models.IntegerField(
verbose_name=_("Unit price")
verbose_name=_("Unit price"),
)
@property
@ -257,6 +261,7 @@ class SpecialTransactionProxy(models.Model):
Remittance,
on_delete=models.PROTECT,
null=True,
related_name="transaction_proxies",
verbose_name=_("Remittance"),
)
@ -335,6 +340,11 @@ class SogeCredit(models.Model):
@transaction.atomic
def save(self, *args, **kwargs):
# This is a pre-registered user that declared that a SoGé account was opened.
# No note exists yet.
if not NoteUser.objects.filter(user=self.user).exists():
return super().save(*args, **kwargs)
if not self.credit_transaction:
credit_transaction = SpecialTransaction(
source=NoteSpecial.objects.get(special_type="Virement bancaire"),
@ -375,6 +385,11 @@ class SogeCredit(models.Model):
tr.valid = True
tr.created_at = timezone.now()
tr.save()
if self.credit_transaction:
# If the soge credit is deleted while the user is not validated yet,
# there is not credit transaction.
# There is a credit transaction iff the user declares that no bank account
# was opened after the validation of the account.
self.credit_transaction.valid = False
self.credit_transaction.reason += " (invalide)"
self.credit_transaction.save()

View File

@ -10,9 +10,8 @@ def save_special_transaction(instance, created, **kwargs):
"""
if not hasattr(instance, "_no_signal"):
if instance.is_credit():
if created and RemittanceType.objects.filter(note=instance.source).exists():
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save()
else:
if created and RemittanceType.objects.filter(note=instance.destination).exists():
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save()
if created and RemittanceType.objects.filter(
note=instance.source if instance.is_credit() else instance.destination).exists():
proxy = SpecialTransactionProxy(transaction=instance, remittance=None)
proxy._force_save = True
proxy.save()

View File

@ -109,9 +109,6 @@ class SpecialTransactionTable(tables.Table):
'a': {'class': 'btn btn-primary btn-danger'}
}, )
def render_id(self, record):
return record.specialtransactionproxy.pk
def render_amount(self, value):
return pretty_money(value)
@ -147,4 +144,4 @@ class SogeCreditTable(tables.Table):
class Meta:
model = SogeCredit
fields = ('user', 'amount', 'valid', )
fields = ('user', 'user__last_name', 'user__first_name', 'amount', 'valid', )

View File

@ -1,4 +1,5 @@
{% load escape_tex %}
{% load escape_tex i18n %}
{% language fr %}
\documentclass[a4paper,11pt]{article}
\usepackage{fontspec}
@ -176,3 +177,4 @@ TVA non applicable, article 293 B du CGI.
\end{center}
\end{document}
{% endlanguage %}

View File

@ -11,8 +11,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd>
<dt class="col-xl-6 text-right">{% trans 'last name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.last_name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'first name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.first_name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user.username }}</a></dd>
{% if "note.view_note_balance"|has_perm:object.user.note %}
<dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt>

View File

@ -60,7 +60,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
let pattern = searchbar_obj.val();
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
invalid_only_obj.is(':checked') ? "" : "&valid=1") + " #credits_table");
$(".table-row").click(function () {
window.document.location = $(this).data("href");

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db.models import Q
@ -8,7 +9,10 @@ from django.test import TestCase
from django.urls import reverse
from member.models import Membership, Club
from note.models import SpecialTransaction, NoteSpecial, Transaction
from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit
from ..api.views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, \
SogeCreditViewSet
from ..models import Invoice, Product, Remittance, RemittanceType, SogeCredit
class TestInvoices(TestCase):
@ -366,11 +370,8 @@ class TestSogeCredits(TestCase):
response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)))
self.assertEqual(response.status_code, 200)
try:
self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
raise AssertionError("It is not possible to delete the soge credit until the note is not credited.")
except ValidationError:
pass
self.assertRaises(ValidationError, self.client.post,
reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True))
SpecialTransaction.objects.create(
source=NoteSpecial.objects.get(special_type="Carte bancaire"),
@ -399,3 +400,82 @@ class TestSogeCredits(TestCase):
"""
response = self.client.get("/api/treasury/soge_credit/")
self.assertEqual(response.status_code, 200)
class TestTreasuryAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.invoice = Invoice.objects.create(
id=1,
object="Object",
description="Description",
name="Me",
address="Earth",
acquitted=False,
)
self.product = Product.objects.create(
invoice=self.invoice,
designation="Product",
quantity=3,
amount=3.14,
)
self.credit = SpecialTransaction.objects.create(
source=NoteSpecial.objects.get(special_type="Chèque"),
destination=self.user.note,
amount=4200,
reason="Credit",
last_name="TOTO",
first_name="Toto",
bank="Société générale",
)
self.remittance = Remittance.objects.create(
remittance_type=RemittanceType.objects.get(),
comment="Test remittance",
closed=False,
)
self.credit.specialtransactionproxy.remittance = self.remittance
self.credit.specialtransactionproxy.save()
self.kfet = Club.objects.get(name="Kfet")
self.bde = self.kfet.parent_club
self.kfet_membership = Membership(
user=self.user,
club=self.kfet,
)
self.kfet_membership._force_renew_parent = True
self.kfet_membership._soge = True
self.kfet_membership.save()
def test_invoice_api(self):
"""
Load Invoice API page and test all filters and permissions
"""
self.check_viewset(InvoiceViewSet, "/api/treasury/invoice/")
def test_product_api(self):
"""
Load Product API page and test all filters and permissions
"""
self.check_viewset(ProductViewSet, "/api/treasury/product/")
def test_remittance_api(self):
"""
Load Remittance API page and test all filters and permissions
"""
self.check_viewset(RemittanceViewSet, "/api/treasury/remittance/")
def test_remittance_type_api(self):
"""
Load RemittanceType API page and test all filters and permissions
"""
self.check_viewset(RemittanceTypeViewSet, "/api/treasury/remittance_type/")
def test_sogecredit_api(self):
"""
Load SogeCredit API page and test all filters and permissions
"""
self.check_viewset(SogeCreditViewSet, "/api/treasury/soge_credit/")

View File

@ -210,7 +210,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
del tex
# The file has to be rendered twice
for ignored in range(2):
for _ignored in range(2):
error = subprocess.Popen(
["/usr/bin/xelatex", "-interaction=nonstopmode", "invoice-{}.tex".format(pk)],
cwd=tmp_dir,
@ -431,7 +431,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
if "valid" not in self.request.GET or not self.request.GET["valid"]:
qs = qs.filter(credit_transaction__valid=False)
return qs[:20]
return qs
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):

View File

@ -1,7 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from rest_framework.filters import OrderingFilter, SearchFilter
from api.viewsets import ReadProtectedModelViewSet
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
@ -15,11 +16,14 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/club/
"""
queryset = WEIClub.objects.all()
queryset = WEIClub.objects.order_by('id')
serializer_class = WEIClubSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'year', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
'membership_end', ]
search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ]
class BusViewSet(ReadProtectedModelViewSet):
@ -28,11 +32,11 @@ class BusViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/bus/
"""
queryset = Bus.objects
queryset = Bus.objects.order_by('id')
serializer_class = BusSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'wei', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'wei', 'description', ]
search_fields = ['$name', '$wei__name', '$description', ]
class BusTeamViewSet(ReadProtectedModelViewSet):
@ -41,11 +45,11 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/team/
"""
queryset = BusTeam.objects
queryset = BusTeam.objects.order_by('id')
serializer_class = BusTeamSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$name', ]
filterset_fields = ['name', 'bus', 'bus__wei', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
class WEIRoleViewSet(ReadProtectedModelViewSet):
@ -54,9 +58,10 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/role/
"""
queryset = WEIRole.objects
queryset = WEIRole.objects.order_by('id')
serializer_class = WEIRoleSerializer
filter_backends = [SearchFilter]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', 'permissions', 'memberships', ]
search_fields = ['$name', ]
@ -66,11 +71,17 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer,
then render it on /api/wei/registration/
"""
queryset = WEIRegistration.objects
queryset = WEIRegistration.objects.order_by('id')
serializer_class = WEIRegistrationSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$user__username', ]
filterset_fields = ['user', 'wei', ]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name',
'emergency_contact_phone', ]
search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email',
'$user__note__alias__name', '$user__note__alias__normalized_name', '$wei__name',
'$wei__email', '$health_issues', '$emergency_contact_name', '$emergency_contact_phone', ]
class WEIMembershipViewSet(ReadProtectedModelViewSet):
@ -79,8 +90,16 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer,
then render it on /api/wei/membership/
"""
queryset = WEIMembership.objects
queryset = WEIMembership.objects.order_by('id')
serializer_class = WEIMembershipSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['$user__username', '$bus__name', '$team__name', ]
filterset_fields = ['user', 'club', 'bus', 'team', ]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
'club__note__alias__normalized_name', 'user__username', 'user__last_name',
'user__first_name', 'user__email', 'user__note__alias__name',
'user__note__alias__normalized_name', 'date_start', 'date_end', 'fee', 'roles', 'bus',
'bus__name', 'team', 'team__name', 'registration', ]
ordering_fields = ['id', 'date_start', 'date_end', ]
search_fields = ['$club__name', '$club__email', '$club__note__alias__name',
'$club__note__alias__normalized_name', '$user__username', '$user__last_name',
'$user__first_name', '$user__email', '$user__note__alias__name',
'$user__note__alias__normalized_name', '$roles__name', '$bus__name', '$team__name', ]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2021, unique=True, verbose_name='year'),
),
migrations.AlterField(
model_name='weiregistration',
name='information_json',
field=models.TextField(default='{}', help_text='Information about the registration (buses for old members, survey for the new members), encoded in JSON', verbose_name='registration information'),
),
]

View File

@ -61,10 +61,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd>
{% endif %}
{% if "note.change_alias"|has_perm:club.note.alias_set.first %}
{% if "note.change_alias"|has_perm:club.note.alias.first %}
<dt class="col-xl-4"><a
href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-8 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd>
<dd class="col-xl-8 text-truncate">{{ club.note.alias.all|join:", " }}</dd>
{% endif %}
<dt class="col-xl-4">{% trans 'email'|capfirst %}</dt>

View File

@ -4,16 +4,19 @@
import subprocess
from datetime import timedelta, date
from api.tests import TestAPI
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Q
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from member.models import Membership
from member.models import Membership, Club
from note.models import NoteClub, SpecialTransaction
from treasury.models import SogeCredit
from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \
WEIRoleViewSet
from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey
from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership
@ -524,7 +527,7 @@ class TestWEIRegistration(TestCase):
sess["permission_mask"] = 0
sess.save()
response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk)))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 403)
sess["permission_mask"] = 42
sess.save()
@ -807,3 +810,97 @@ class TestWEISurveyAlgorithm(TestCase):
def test_survey_algorithm(self):
CurrentSurvey.get_algorithm_class()().run_algorithm()
class TestWeiAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.year = timezone.now().year
self.wei = WEIClub.objects.create(
name="Test WEI",
email="gc.wei@example.com",
parent_club_id=2,
membership_fee_paid=12500,
membership_fee_unpaid=5500,
membership_start=date(self.year, 1, 1),
membership_end=date(self.year, 12, 31),
membership_duration=396,
year=self.year,
date_start=date.today() + timedelta(days=2),
date_end=date(self.year, 12, 31),
)
NoteClub.objects.create(club=self.wei)
self.bus = Bus.objects.create(
name="Test Bus",
wei=self.wei,
description="Test Bus",
)
self.team = BusTeam.objects.create(
name="Test Team",
bus=self.bus,
color=0xFFFFFF,
description="Test Team",
)
self.registration = WEIRegistration.objects.create(
user_id=self.user.id,
wei_id=self.wei.id,
soge_credit=True,
caution_check=True,
birth_date=date(2000, 1, 1),
gender="nonbinary",
clothing_cut="male",
clothing_size="XL",
health_issues="I am a bot",
emergency_contact_name="Pikachu",
emergency_contact_phone="+33123456789",
first_year=False,
)
Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE"))
Membership.objects.create(user=self.user, club=Club.objects.get(name="Kfet"))
self.membership = WEIMembership.objects.create(
user=self.user,
club=self.wei,
fee=125,
bus=self.bus,
team=self.team,
registration=self.registration,
)
self.membership.roles.add(WEIRole.objects.last())
self.membership.save()
def test_weiclub_api(self):
"""
Load WEI API page and test all filters and permissions
"""
self.check_viewset(WEIClubViewSet, "/api/wei/club/")
def test_wei_bus_api(self):
"""
Load Bus API page and test all filters and permissions
"""
self.check_viewset(BusViewSet, "/api/wei/bus/")
def test_wei_team_api(self):
"""
Load BusTeam API page and test all filters and permissions
"""
self.check_viewset(BusTeamViewSet, "/api/wei/team/")
def test_weirole_api(self):
"""
Load WEIRole API page and test all filters and permissions
"""
self.check_viewset(WEIRoleViewSet, "/api/wei/role/")
def test_weiregistration_api(self):
"""
Load WEIRegistration API page and test all filters and permissions
"""
self.check_viewset(WEIRegistrationViewSet, "/api/wei/registration/")
def test_weimembership_api(self):
"""
Load WEIMembership API page and test all filters and permissions
"""
self.check_viewset(WEIMembershipViewSet, "/api/wei/membership/")

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/_static/img/create_transaction.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

344
docs/_static/img/graphs/activity.svg vendored Normal file
View File

@ -0,0 +1,344 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="578pt" height="729pt"
viewBox="0.00 0.00 578.00 729.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 725)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-725 574,-725 574,4 -4,4"/>
<!-- activity_models_ActivityType -->
<g id="node1" class="node">
<title>activity_models_ActivityType</title>
<polygon fill="white" stroke="transparent" points="8,-4 8,-92 186,-92 186,-4 8,-4"/>
<polygon fill="#1b563f" stroke="transparent" points="9,-70 9,-91 185,-91 185,-70 9,-70"/>
<text text-anchor="start" x="51" y="-79" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="61" y="-79" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;ActivityType &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-62.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-62.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="29" y="-62.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="93" y="-62.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="103" y="-62.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="141" y="-62.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-49.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-49.6" font-family="Roboto" font-size="8.00">can_invite</text>
<text text-anchor="start" x="57" y="-49.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="93" y="-49.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="103" y="-49.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="149" y="-49.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-36.6" font-family="Roboto" font-size="8.00">guest_entry_fee</text>
<text text-anchor="start" x="79" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="93" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="103" y="-36.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="173" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-23.6" font-family="Roboto" font-size="8.00">manage_entries</text>
<text text-anchor="start" x="79" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="93" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="103" y="-23.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="149" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-10.6" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="42" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="93" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="103" y="-10.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="138" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="8,-4 8,-92 186,-92 186,-4 8,-4"/>
</g>
<!-- activity_models_Activity -->
<g id="node2" class="node">
<title>activity_models_Activity</title>
<polygon fill="white" stroke="transparent" points="197,-145 197,-324 369,-324 369,-145 197,-145"/>
<polygon fill="#1b563f" stroke="transparent" points="198,-301.5 198,-322.5 368,-322.5 368,-301.5 198,-301.5"/>
<text text-anchor="start" x="248" y="-310.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="258" y="-310.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Activity &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-294.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-294.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="218" y="-294.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-294.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-294.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="333" y="-294.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-281.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-281.1" font-family="Roboto" font-weight="bold" font-size="8.00">activity_type</text>
<text text-anchor="start" x="261" y="-281.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-281.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-281.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="356" y="-281.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-268.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-268.1" font-family="Roboto" font-weight="bold" font-size="8.00">attendees_club</text>
<text text-anchor="start" x="271" y="-268.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-268.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-268.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="356" y="-268.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-255.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-255.1" font-family="Roboto" font-weight="bold" font-size="8.00">creater</text>
<text text-anchor="start" x="238" y="-255.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-255.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-255.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="356" y="-255.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-242.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-242.1" font-family="Roboto" font-weight="bold" font-size="8.00">organizer</text>
<text text-anchor="start" x="247" y="-242.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-242.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-242.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="356" y="-242.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-229.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-229.1" font-family="Roboto" font-size="8.00">date_end</text>
<text text-anchor="start" x="244" y="-229.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-229.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-229.1" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="347" y="-229.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-216.1" font-family="Roboto" font-size="8.00">date_start</text>
<text text-anchor="start" x="247" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-216.1" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="347" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-203.1" font-family="Roboto" font-size="8.00">description</text>
<text text-anchor="start" x="249" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-203.1" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="327" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-190.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">location</text>
<text text-anchor="start" x="238" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-190.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="330" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-177.1" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="231" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-177.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="330" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-164.1" font-family="Roboto" font-size="8.00">open</text>
<text text-anchor="start" x="229" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-164.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="341" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="200" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="210" y="-151.1" font-family="Roboto" font-size="8.00">valid</text>
<text text-anchor="start" x="226" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="285" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="295" y="-151.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="341" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="197,-145 197,-324 369,-324 369,-145 197,-145"/>
</g>
<!-- activity_models_Activity&#45;&gt;activity_models_ActivityType -->
<g id="edge1" class="edge">
<title>activity_models_Activity&#45;&gt;activity_models_ActivityType</title>
<path fill="none" stroke="black" d="M183.88,-135.18C170.27,-121.68 156.83,-108.35 144.74,-96.35"/>
<ellipse fill="black" stroke="black" cx="186.89" cy="-138.16" rx="4" ry="4"/>
<text text-anchor="middle" x="197" y="-116.6" font-family="Roboto" font-size="8.00"> activity_type (+)</text>
</g>
<!-- django_contrib_auth_models_User -->
<g id="node6" class="node">
<title>django_contrib_auth_models_User</title>
<polygon fill="white" stroke="transparent" points="220,-37.5 220,-58.5 264,-58.5 264,-37.5 220,-37.5"/>
<polygon fill="#1b563f" stroke="transparent" points="220,-37 220,-58 264,-58 264,-37 220,-37"/>
<text text-anchor="start" x="224" y="-45.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="229" y="-45.4" font-family="Roboto" font-size="12.00" fill="white">User</text>
<text text-anchor="start" x="255" y="-45.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- activity_models_Activity&#45;&gt;django_contrib_auth_models_User -->
<g id="edge2" class="edge">
<title>activity_models_Activity&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M245.08,-133.07C244.3,-129.69 243.61,-126.33 243,-123 239.5,-103.79 239.8,-81.3 240.63,-66.2"/>
<ellipse fill="black" stroke="black" cx="246.06" cy="-137.08" rx="4" ry="4"/>
<text text-anchor="middle" x="272.5" y="-116.6" font-family="Roboto" font-size="8.00"> creater (activity)</text>
</g>
<!-- member_models_Club -->
<g id="node7" class="node">
<title>member_models_Club</title>
<polygon fill="white" stroke="transparent" points="338,-37.5 338,-58.5 382,-58.5 382,-37.5 338,-37.5"/>
<polygon fill="#1b563f" stroke="transparent" points="338,-37 338,-58 382,-58 382,-37 338,-37"/>
<text text-anchor="start" x="342" y="-45.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="347" y="-45.4" font-family="Roboto" font-size="12.00" fill="white">Club</text>
<text text-anchor="start" x="373" y="-45.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- activity_models_Activity&#45;&gt;member_models_Club -->
<g id="edge3" class="edge">
<title>activity_models_Activity&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M316.94,-133.22C319.56,-126.67 322.25,-120.22 325,-114 332.41,-97.23 342.46,-78.92 349.89,-66.01"/>
<ellipse fill="black" stroke="black" cx="315.37" cy="-137.22" rx="4" ry="4"/>
<text text-anchor="middle" x="349.5" y="-116.6" font-family="Roboto" font-size="8.00"> organizer (+)</text>
</g>
<!-- activity_models_Activity&#45;&gt;member_models_Club -->
<g id="edge4" class="edge">
<title>activity_models_Activity&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M369.62,-133.72C371.23,-130.19 372.7,-126.61 374,-123 380.76,-104.17 374.44,-81.42 368.26,-66.15"/>
<ellipse fill="black" stroke="black" cx="367.85" cy="-137.32" rx="4" ry="4"/>
<text text-anchor="middle" x="412" y="-116.6" font-family="Roboto" font-size="8.00"> attendees_club (+)</text>
</g>
<!-- activity_models_Entry -->
<g id="node3" class="node">
<title>activity_models_Entry</title>
<polygon fill="white" stroke="transparent" points="284,-518 284,-606 448,-606 448,-518 284,-518"/>
<polygon fill="#1b563f" stroke="transparent" points="285,-584 285,-605 447,-605 447,-584 285,-584"/>
<text text-anchor="start" x="336.5" y="-593" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="346.5" y="-593" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Entry &#160;&#160;&#160;</text>
<text text-anchor="start" x="287" y="-576.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297" y="-576.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="305" y="-576.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="340" y="-576.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="350" y="-576.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="388" y="-576.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287" y="-563.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297" y="-563.6" font-family="Roboto" font-weight="bold" font-size="8.00">activity</text>
<text text-anchor="start" x="326" y="-563.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="340" y="-563.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="350" y="-563.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="411" y="-563.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287" y="-550.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297" y="-550.6" font-family="Roboto" font-weight="bold" font-size="8.00">guest</text>
<text text-anchor="start" x="320" y="-550.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="340" y="-550.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="350" y="-550.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="426" y="-550.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287" y="-537.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297" y="-537.6" font-family="Roboto" font-weight="bold" font-size="8.00">note</text>
<text text-anchor="start" x="315" y="-537.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="340" y="-537.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="350" y="-537.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (note_ptr)</text>
<text text-anchor="start" x="435" y="-537.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287" y="-524.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297" y="-524.6" font-family="Roboto" font-size="8.00">time</text>
<text text-anchor="start" x="313" y="-524.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="340" y="-524.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="350" y="-524.6" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="402" y="-524.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="284,-518 284,-606 448,-606 448,-518 284,-518"/>
</g>
<!-- activity_models_Entry&#45;&gt;activity_models_Activity -->
<g id="edge5" class="edge">
<title>activity_models_Entry&#45;&gt;activity_models_Activity</title>
<path fill="none" stroke="black" d="M268.49,-526.87C243.03,-513.01 218.65,-494.17 204,-469 178.65,-425.45 193.33,-372.56 216.59,-328.14"/>
<ellipse fill="black" stroke="black" cx="272.15" cy="-528.79" rx="4" ry="4"/>
<text text-anchor="middle" x="233" y="-419.1" font-family="Roboto" font-size="8.00"> activity (entries)</text>
</g>
<!-- activity_models_Guest -->
<g id="node4" class="node">
<title>activity_models_Guest</title>
<polygon fill="white" stroke="transparent" points="279.5,-377 279.5,-465 452.5,-465 452.5,-377 279.5,-377"/>
<polygon fill="#1b563f" stroke="transparent" points="281,-443 281,-464 452,-464 452,-443 281,-443"/>
<text text-anchor="start" x="335.5" y="-452" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="345.5" y="-452" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Guest &#160;&#160;&#160;</text>
<text text-anchor="start" x="283" y="-435.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="293" y="-435.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="301" y="-435.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="345" y="-435.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="355" y="-435.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="393" y="-435.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="283" y="-422.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="293" y="-422.6" font-family="Roboto" font-weight="bold" font-size="8.00">activity</text>
<text text-anchor="start" x="322" y="-422.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="345" y="-422.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="355" y="-422.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="416" y="-422.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="283" y="-409.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="293" y="-409.6" font-family="Roboto" font-weight="bold" font-size="8.00">inviter</text>
<text text-anchor="start" x="318" y="-409.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="345" y="-409.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="355" y="-409.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (note_ptr)</text>
<text text-anchor="start" x="440" y="-409.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="283" y="-396.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="293" y="-396.6" font-family="Roboto" font-size="8.00">first_name</text>
<text text-anchor="start" x="331" y="-396.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="345" y="-396.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="355" y="-396.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="390" y="-396.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="283" y="-383.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="293" y="-383.6" font-family="Roboto" font-size="8.00">last_name</text>
<text text-anchor="start" x="330" y="-383.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="345" y="-383.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="355" y="-383.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="390" y="-383.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="279.5,-377 279.5,-465 452.5,-465 452.5,-377 279.5,-377"/>
</g>
<!-- activity_models_Entry&#45;&gt;activity_models_Guest -->
<g id="edge7" class="edge">
<title>activity_models_Entry&#45;&gt;activity_models_Guest</title>
<path fill="none" stroke="black" d="M366,-513.94C366,-499.45 366,-483.5 366,-469.01"/>
<text text-anchor="middle" x="390.5" y="-489.6" font-family="Roboto" font-size="8.00"> guest (entry)</text>
</g>
<!-- note_models_notes_NoteUser -->
<g id="node8" class="node">
<title>note_models_notes_NoteUser</title>
<polygon fill="white" stroke="transparent" points="411,-224 411,-245 481,-245 481,-224 411,-224"/>
<polygon fill="#1b563f" stroke="transparent" points="411,-223.5 411,-244.5 481,-244.5 481,-223.5 411,-223.5"/>
<text text-anchor="start" x="415" y="-231.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="420" y="-231.9" font-family="Roboto" font-size="12.00" fill="white">NoteUser</text>
<text text-anchor="start" x="472" y="-231.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- activity_models_Entry&#45;&gt;note_models_notes_NoteUser -->
<g id="edge6" class="edge">
<title>activity_models_Entry&#45;&gt;note_models_notes_NoteUser</title>
<path fill="none" stroke="black" d="M442.25,-507.94C453.52,-496.48 463.51,-483.41 470,-469 503.73,-394.13 469.49,-292.46 453.39,-252.56"/>
<ellipse fill="black" stroke="black" cx="439.12" cy="-510.99" rx="4" ry="4"/>
<text text-anchor="middle" x="506.5" y="-419.1" font-family="Roboto" font-size="8.00"> note (entry)</text>
</g>
<!-- activity_models_Guest&#45;&gt;activity_models_Activity -->
<g id="edge8" class="edge">
<title>activity_models_Guest&#45;&gt;activity_models_Activity</title>
<path fill="none" stroke="black" d="M341.43,-365.38C336.13,-353.6 330.39,-340.84 324.64,-328.06"/>
<ellipse fill="black" stroke="black" cx="343.2" cy="-369.32" rx="4" ry="4"/>
<text text-anchor="middle" x="354.5" y="-348.6" font-family="Roboto" font-size="8.00"> activity (+)</text>
</g>
<!-- activity_models_Guest&#45;&gt;note_models_notes_NoteUser -->
<g id="edge9" class="edge">
<title>activity_models_Guest&#45;&gt;note_models_notes_NoteUser</title>
<path fill="none" stroke="black" d="M389.66,-365.44C406.3,-327.05 427.51,-278.15 438.55,-252.68"/>
<ellipse fill="black" stroke="black" cx="387.99" cy="-369.3" rx="4" ry="4"/>
<text text-anchor="middle" x="424.5" y="-348.6" font-family="Roboto" font-size="8.00"> inviter (guests)</text>
</g>
<!-- activity_models_GuestTransaction -->
<g id="node5" class="node">
<title>activity_models_GuestTransaction</title>
<polygon fill="white" stroke="transparent" points="350.5,-668 350.5,-717 537.5,-717 537.5,-668 350.5,-668"/>
<polygon fill="#1b563f" stroke="transparent" points="352,-694.5 352,-715.5 537,-715.5 537,-694.5 352,-694.5"/>
<text text-anchor="start" x="386.5" y="-703.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="396.5" y="-703.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;GuestTransaction &#160;&#160;&#160;</text>
<text text-anchor="start" x="354" y="-687.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="364" y="-687.1" font-family="Roboto" font-weight="bold" font-size="8.00">transaction_ptr</text>
<text text-anchor="start" x="425" y="-687.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="439" y="-687.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="449" y="-687.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="525" y="-687.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="354" y="-674.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="364" y="-674.1" font-family="Roboto" font-weight="bold" font-size="8.00">entry</text>
<text text-anchor="start" x="385" y="-674.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="439" y="-674.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="449" y="-674.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="525" y="-674.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="350.5,-668 350.5,-717 537.5,-717 537.5,-668 350.5,-668"/>
</g>
<!-- activity_models_GuestTransaction&#45;&gt;activity_models_Entry -->
<g id="edge10" class="edge">
<title>activity_models_GuestTransaction&#45;&gt;activity_models_Entry</title>
<path fill="none" stroke="black" d="M423.89,-663.91C419.84,-658.07 415.69,-651.89 412,-646 404.83,-634.55 397.53,-621.92 390.93,-610.04"/>
<text text-anchor="middle" x="456" y="-635.1" font-family="Roboto" font-size="8.00"> entry (guesttransaction)</text>
</g>
<!-- note_models_transactions_Transaction -->
<g id="node9" class="node">
<title>note_models_transactions_Transaction</title>
<polygon fill="white" stroke="transparent" points="482,-551.5 482,-572.5 562,-572.5 562,-551.5 482,-551.5"/>
<polygon fill="#1b563f" stroke="transparent" points="482,-551 482,-572 562,-572 562,-551 482,-551"/>
<text text-anchor="start" x="486" y="-559.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="491" y="-559.4" font-family="Roboto" font-size="12.00" fill="white">Transaction</text>
<text text-anchor="start" x="553" y="-559.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- activity_models_GuestTransaction&#45;&gt;note_models_transactions_Transaction -->
<g id="edge11" class="edge">
<title>activity_models_GuestTransaction&#45;&gt;note_models_transactions_Transaction</title>
<path fill="none" stroke="black" d="M484.47,-663.95C490.33,-658.55 495.78,-652.52 500,-646 510.86,-629.2 516.38,-607.22 519.17,-590.09"/>
<polygon fill="none" stroke="black" points="522.64,-590.54 520.58,-580.15 515.71,-589.56 522.64,-590.54"/>
<text text-anchor="middle" x="528" y="-639.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="528" y="-630.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

108
docs/_static/img/graphs/logs.svg vendored Normal file
View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="209pt" height="237pt"
viewBox="0.00 0.00 208.50 237.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 233)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-233 204.5,-233 204.5,4 -4,4"/>
<!-- logs_models_Changelog -->
<g id="node1" class="node">
<title>logs_models_Changelog</title>
<polygon fill="white" stroke="transparent" points="8,-85 8,-225 181,-225 181,-85 8,-85"/>
<polygon fill="#1b563f" stroke="transparent" points="9.5,-203 9.5,-224 180.5,-224 180.5,-203 9.5,-203"/>
<text text-anchor="start" x="52" y="-212" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="62" y="-212" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Changelog &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-195.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-195.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="29.5" y="-195.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-195.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-195.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="126.5" y="-195.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-182.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-182.6" font-family="Roboto" font-weight="bold" font-size="8.00">model</text>
<text text-anchor="start" x="46.5" y="-182.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-182.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-182.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="149.5" y="-182.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-169.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-169.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="39.5" y="-169.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-169.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-169.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="149.5" y="-169.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-156.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-156.6" font-family="Roboto" font-size="8.00">action</text>
<text text-anchor="start" x="43.5" y="-156.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-156.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-156.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="123.5" y="-156.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-143.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-143.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">data</text>
<text text-anchor="start" x="37.5" y="-143.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-143.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-143.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">TextField</text>
<text text-anchor="start" x="120.5" y="-143.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-130.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-130.6" font-family="Roboto" font-size="8.00">instance_pk</text>
<text text-anchor="start" x="64.5" y="-130.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-130.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-130.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="123.5" y="-130.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-117.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-117.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">ip</text>
<text text-anchor="start" x="28.5" y="-117.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-117.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-117.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">GenericIPAddressField</text>
<text text-anchor="start" x="168.5" y="-117.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-104.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-104.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">previous</text>
<text text-anchor="start" x="51.5" y="-104.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-104.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-104.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">TextField</text>
<text text-anchor="start" x="120.5" y="-104.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-91.6" font-family="Roboto" font-size="8.00">timestamp</text>
<text text-anchor="start" x="58.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="88.5" y="-91.6" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="140.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="8,-85 8,-225 181,-225 181,-85 8,-85"/>
</g>
<!-- django_contrib_auth_models_User -->
<g id="node2" class="node">
<title>django_contrib_auth_models_User</title>
<polygon fill="white" stroke="transparent" points="23.5,-7.5 23.5,-28.5 67.5,-28.5 67.5,-7.5 23.5,-7.5"/>
<polygon fill="#1b563f" stroke="transparent" points="23.5,-7 23.5,-28 67.5,-28 67.5,-7 23.5,-7"/>
<text text-anchor="start" x="27.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="32.5" y="-15.4" font-family="Roboto" font-size="12.00" fill="white">User</text>
<text text-anchor="start" x="58.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- logs_models_Changelog&#45;&gt;django_contrib_auth_models_User -->
<g id="edge1" class="edge">
<title>logs_models_Changelog&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M62.09,-73.17C60.86,-69.74 59.65,-66.34 58.5,-63 55.49,-54.26 52.54,-44.41 50.21,-36.25"/>
<ellipse fill="black" stroke="black" cx="63.48" cy="-76.95" rx="4" ry="4"/>
<text text-anchor="middle" x="90.5" y="-56.6" font-family="Roboto" font-size="8.00"> user (changelog)</text>
</g>
<!-- django_contrib_contenttypes_models_ContentType -->
<g id="node3" class="node">
<title>django_contrib_contenttypes_models_ContentType</title>
<polygon fill="white" stroke="transparent" points="101.5,-7.5 101.5,-28.5 187.5,-28.5 187.5,-7.5 101.5,-7.5"/>
<polygon fill="#1b563f" stroke="transparent" points="101.5,-7 101.5,-28 187.5,-28 187.5,-7 101.5,-7"/>
<text text-anchor="start" x="105.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="110.5" y="-15.4" font-family="Roboto" font-size="12.00" fill="white">ContentType</text>
<text text-anchor="start" x="178.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- logs_models_Changelog&#45;&gt;django_contrib_contenttypes_models_ContentType -->
<g id="edge2" class="edge">
<title>logs_models_Changelog&#45;&gt;django_contrib_contenttypes_models_ContentType</title>
<path fill="none" stroke="black" d="M124.44,-73.16C129.61,-59.21 134.48,-46.05 138.13,-36.2"/>
<ellipse fill="black" stroke="black" cx="122.94" cy="-77.22" rx="4" ry="4"/>
<text text-anchor="middle" x="165.5" y="-56.6" font-family="Roboto" font-size="8.00"> model (changelog)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

279
docs/_static/img/graphs/member.svg vendored Normal file
View File

@ -0,0 +1,279 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="601pt" height="440pt"
viewBox="0.00 0.00 600.50 440.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 436)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-436 596.5,-436 596.5,4 -4,4"/>
<!-- member_models_Profile -->
<g id="node1" class="node">
<title>member_models_Profile</title>
<polygon fill="white" stroke="transparent" points="8,-210 8,-428 227,-428 227,-210 8,-210"/>
<polygon fill="#1b563f" stroke="transparent" points="9.5,-406 9.5,-427 226.5,-427 226.5,-406 9.5,-406"/>
<text text-anchor="start" x="85.5" y="-415" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="95.5" y="-415" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Profile &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-398.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-398.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="29.5" y="-398.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-398.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-398.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="163.5" y="-398.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-385.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-385.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="39.5" y="-385.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-385.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-385.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="201.5" y="-385.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-372.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-372.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">address</text>
<text text-anchor="start" x="49.5" y="-372.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-372.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-372.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="160.5" y="-372.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-359.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-359.6" font-family="Roboto" font-size="8.00">department</text>
<text text-anchor="start" x="63.5" y="-359.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-359.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-359.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="160.5" y="-359.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-346.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-346.6" font-family="Roboto" font-size="8.00">email_confirmed</text>
<text text-anchor="start" x="80.5" y="-346.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-346.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-346.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="171.5" y="-346.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-333.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-333.6" font-family="Roboto" font-size="8.00">last_report</text>
<text text-anchor="start" x="59.5" y="-333.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-333.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-333.6" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="177.5" y="-333.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-320.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-320.6" font-family="Roboto" font-size="8.00">ml_art_registration</text>
<text text-anchor="start" x="88.5" y="-320.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-320.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-320.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="171.5" y="-320.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-307.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-307.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">ml_events_registration</text>
<text text-anchor="start" x="101.5" y="-307.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-307.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-307.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="160.5" y="-307.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-294.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-294.6" font-family="Roboto" font-size="8.00">ml_sport_registration</text>
<text text-anchor="start" x="96.5" y="-294.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-294.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-294.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="171.5" y="-294.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-281.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-281.6" font-family="Roboto" font-size="8.00">paid</text>
<text text-anchor="start" x="37.5" y="-281.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-281.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-281.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="171.5" y="-281.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-268.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-268.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">phone_number</text>
<text text-anchor="start" x="76.5" y="-268.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-268.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-268.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">PhoneNumberField</text>
<text text-anchor="start" x="195.5" y="-268.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-255.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-255.6" font-family="Roboto" font-size="8.00">promotion</text>
<text text-anchor="start" x="57.5" y="-255.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-255.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-255.6" font-family="Roboto" font-size="8.00">PositiveSmallIntegerField</text>
<text text-anchor="start" x="214.5" y="-255.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-242.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-242.6" font-family="Roboto" font-size="8.00">registration_valid</text>
<text text-anchor="start" x="81.5" y="-242.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-242.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-242.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="171.5" y="-242.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-229.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-229.6" font-family="Roboto" font-size="8.00">report_frequency</text>
<text text-anchor="start" x="82.5" y="-229.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-229.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-229.6" font-family="Roboto" font-size="8.00">PositiveSmallIntegerField</text>
<text text-anchor="start" x="214.5" y="-229.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-216.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-216.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">section</text>
<text text-anchor="start" x="46.5" y="-216.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.5" y="-216.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.5" y="-216.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="160.5" y="-216.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="8,-210 8,-428 227,-428 227,-210 8,-210"/>
</g>
<!-- django_contrib_auth_models_User -->
<g id="node4" class="node">
<title>django_contrib_auth_models_User</title>
<polygon fill="white" stroke="transparent" points="125.5,-70 125.5,-91 169.5,-91 169.5,-70 125.5,-70"/>
<polygon fill="#1b563f" stroke="transparent" points="125.5,-69.5 125.5,-90.5 169.5,-90.5 169.5,-69.5 125.5,-69.5"/>
<text text-anchor="start" x="129.5" y="-77.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="134.5" y="-77.9" font-family="Roboto" font-size="12.00" fill="white">User</text>
<text text-anchor="start" x="160.5" y="-77.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- member_models_Profile&#45;&gt;django_contrib_auth_models_User -->
<g id="edge1" class="edge">
<title>member_models_Profile&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M131.71,-205.98C136.98,-164.44 142.38,-121.86 145.3,-98.85"/>
<text text-anchor="middle" x="158.5" y="-181.6" font-family="Roboto" font-size="8.00"> user (profile)</text>
</g>
<!-- member_models_Club -->
<g id="node2" class="node">
<title>member_models_Club</title>
<polygon fill="white" stroke="transparent" points="212,-4 212,-157 421,-157 421,-4 212,-4"/>
<polygon fill="#1b563f" stroke="transparent" points="213.5,-134.5 213.5,-155.5 420.5,-155.5 420.5,-134.5 213.5,-134.5"/>
<text text-anchor="start" x="288.5" y="-143.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="298.5" y="-143.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Club &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-127.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="233.5" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-127.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="376.5" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-114.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">parent_club</text>
<text text-anchor="start" x="273.5" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-114.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">ForeignKey (id)</text>
<text text-anchor="start" x="399.5" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-101.1" font-family="Roboto" font-size="8.00">email</text>
<text text-anchor="start" x="244.5" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-101.1" font-family="Roboto" font-size="8.00">EmailField</text>
<text text-anchor="start" x="375.5" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-88.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">membership_duration</text>
<text text-anchor="start" x="304.5" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-88.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">PositiveIntegerField</text>
<text text-anchor="start" x="408.5" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-75.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">membership_end</text>
<text text-anchor="start" x="288.5" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-75.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">DateField</text>
<text text-anchor="start" x="372.5" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-62.1" font-family="Roboto" font-size="8.00">membership_fee_paid</text>
<text text-anchor="start" x="305.5" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-62.1" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="408.5" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-49.1" font-family="Roboto" font-size="8.00">membership_fee_unpaid</text>
<text text-anchor="start" x="314.5" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-49.1" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="408.5" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-36.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">membership_start</text>
<text text-anchor="start" x="290.5" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-36.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">DateField</text>
<text text-anchor="start" x="372.5" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-23.1" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="246.5" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-23.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="373.5" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="215.5" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-10.1" font-family="Roboto" font-size="8.00">require_memberships</text>
<text text-anchor="start" x="302.5" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="328.5" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="338.5" y="-10.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="384.5" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="212,-4 212,-157 421,-157 421,-4 212,-4"/>
</g>
<!-- member_models_Club&#45;&gt;member_models_Club -->
<g id="edge2" class="edge">
<title>member_models_Club&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M437.02,-93.04C443.29,-89.95 447,-85.77 447,-80.5 447,-73.33 440.12,-68.17 429.26,-65.04"/>
<ellipse fill="black" stroke="black" cx="433" cy="-94.55" rx="4" ry="4"/>
<text text-anchor="middle" x="480.5" y="-78.6" font-family="Roboto" font-size="8.00"> parent_club (club)</text>
</g>
<!-- member_models_Membership -->
<g id="node3" class="node">
<title>member_models_Membership</title>
<polygon fill="white" stroke="transparent" points="261,-268.5 261,-369.5 418,-369.5 418,-268.5 261,-268.5"/>
<polygon fill="#1b563f" stroke="transparent" points="262.5,-347 262.5,-368 417.5,-368 417.5,-347 262.5,-347"/>
<text text-anchor="start" x="294" y="-356" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304" y="-356" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Membership &#160;&#160;&#160;</text>
<text text-anchor="start" x="264.5" y="-339.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="274.5" y="-339.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="282.5" y="-339.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="325.5" y="-339.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="335.5" y="-339.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="373.5" y="-339.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="264.5" y="-326.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="274.5" y="-326.6" font-family="Roboto" font-weight="bold" font-size="8.00">club</text>
<text text-anchor="start" x="292.5" y="-326.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="325.5" y="-326.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="335.5" y="-326.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="396.5" y="-326.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="264.5" y="-313.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="274.5" y="-313.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="292.5" y="-313.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="325.5" y="-313.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="335.5" y="-313.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="396.5" y="-313.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="264.5" y="-300.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="274.5" y="-300.6" font-family="Roboto" font-size="8.00">date_end</text>
<text text-anchor="start" x="308.5" y="-300.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="325.5" y="-300.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="335.5" y="-300.6" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="369.5" y="-300.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="264.5" y="-287.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="274.5" y="-287.6" font-family="Roboto" font-size="8.00">date_start</text>
<text text-anchor="start" x="311.5" y="-287.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="325.5" y="-287.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="335.5" y="-287.6" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="369.5" y="-287.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="264.5" y="-274.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="274.5" y="-274.6" font-family="Roboto" font-size="8.00">fee</text>
<text text-anchor="start" x="286.5" y="-274.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="325.5" y="-274.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="335.5" y="-274.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="405.5" y="-274.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="261,-268.5 261,-369.5 418,-369.5 418,-268.5 261,-268.5"/>
</g>
<!-- member_models_Membership&#45;&gt;member_models_Club -->
<g id="edge4" class="edge">
<title>member_models_Membership&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M333.5,-256.33C330.68,-227.34 327.29,-192.43 324.27,-161.45"/>
<ellipse fill="black" stroke="black" cx="333.89" cy="-260.35" rx="4" ry="4"/>
<text text-anchor="middle" x="362.5" y="-181.6" font-family="Roboto" font-size="8.00"> club (membership)</text>
</g>
<!-- member_models_Membership&#45;&gt;django_contrib_auth_models_User -->
<g id="edge3" class="edge">
<title>member_models_Membership&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M290.49,-258.16C275.79,-240.95 259.35,-222.4 243.5,-206 222.95,-184.74 213.11,-183.97 194.5,-161 178.45,-141.19 164.21,-115.32 155.72,-98.56"/>
<ellipse fill="black" stroke="black" cx="293.24" cy="-261.39" rx="4" ry="4"/>
<text text-anchor="middle" x="261" y="-181.6" font-family="Roboto" font-size="8.00"> user (memberships)</text>
</g>
<!-- permission_models_Role -->
<g id="node5" class="node">
<title>permission_models_Role</title>
<polygon fill="white" stroke="transparent" points="540.5,-70 540.5,-91 584.5,-91 584.5,-70 540.5,-70"/>
<polygon fill="#1b563f" stroke="transparent" points="540.5,-69.5 540.5,-90.5 584.5,-90.5 584.5,-69.5 540.5,-69.5"/>
<text text-anchor="start" x="545" y="-77.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="550" y="-77.9" font-family="Roboto" font-size="12.00" fill="white">Role</text>
<text text-anchor="start" x="575" y="-77.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- member_models_Membership&#45;&gt;permission_models_Role -->
<g id="edge5" class="edge">
<title>member_models_Membership&#45;&gt;permission_models_Role</title>
<path fill="none" stroke="black" d="M422.13,-259.45C456.46,-232.28 494.89,-197.84 523.5,-161 536.38,-144.41 546.62,-122.88 553.37,-106.41"/>
<ellipse fill="black" stroke="black" cx="418.91" cy="-261.97" rx="4" ry="4"/>
<ellipse fill="black" stroke="black" cx="554.93" cy="-102.49" rx="4" ry="4"/>
<text text-anchor="middle" x="542.5" y="-181.6" font-family="Roboto" font-size="8.00"> roles (membership)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

588
docs/_static/img/graphs/note.svg vendored Normal file
View File

@ -0,0 +1,588 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="1306pt" height="872pt"
viewBox="0.00 0.00 1306.00 872.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 868)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-868 1302,-868 1302,4 -4,4"/>
<!-- polymorphic_models_PolymorphicModel -->
<g id="node1" class="node">
<title>polymorphic_models_PolymorphicModel</title>
<polygon fill="white" stroke="transparent" points="254,-85 254,-121 441,-121 441,-85 254,-85"/>
<polygon fill="#1b563f" stroke="transparent" points="255.5,-99 255.5,-120 440.5,-120 440.5,-99 255.5,-99"/>
<text text-anchor="start" x="286.5" y="-108" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="296.5" y="-108" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;PolymorphicModel &#160;&#160;&#160;</text>
<text text-anchor="start" x="257.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="267.5" y="-91.6" font-family="Roboto" font-weight="bold" font-size="8.00">polymorphic_ctype</text>
<text text-anchor="start" x="343.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="357.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="367.5" y="-91.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="428.5" y="-91.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="254,-85 254,-121 441,-121 441,-85 254,-85"/>
</g>
<!-- django_contrib_contenttypes_models_ContentType -->
<g id="node13" class="node">
<title>django_contrib_contenttypes_models_ContentType</title>
<polygon fill="white" stroke="transparent" points="304.5,-7.5 304.5,-28.5 390.5,-28.5 390.5,-7.5 304.5,-7.5"/>
<polygon fill="#1b563f" stroke="transparent" points="304.5,-7 304.5,-28 390.5,-28 390.5,-7 304.5,-7"/>
<text text-anchor="start" x="308.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="313.5" y="-15.4" font-family="Roboto" font-size="12.00" fill="white">ContentType</text>
<text text-anchor="start" x="381.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- polymorphic_models_PolymorphicModel&#45;&gt;django_contrib_contenttypes_models_ContentType -->
<g id="edge1" class="edge">
<title>polymorphic_models_PolymorphicModel&#45;&gt;django_contrib_contenttypes_models_ContentType</title>
<path fill="none" stroke="black" d="M347.5,-72.49C347.5,-60.3 347.5,-46.6 347.5,-36.12"/>
<ellipse fill="black" stroke="black" cx="347.5" cy="-76.63" rx="4" ry="4"/>
<text text-anchor="middle" x="463" y="-56.6" font-family="Roboto" font-size="8.00"> polymorphic_ctype (polymorphic_%(app_label)s.%(class)s_set+)</text>
</g>
<!-- note_models_notes_Note -->
<g id="node2" class="node">
<title>note_models_notes_Note</title>
<polygon fill="white" stroke="transparent" points="366,-183 366,-319 553,-319 553,-183 366,-183"/>
<polygon fill="#1b563f" stroke="transparent" points="367.5,-288 367.5,-318 552.5,-318 552.5,-288 367.5,-288"/>
<text text-anchor="start" x="438" y="-306" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="448" y="-306" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Note</text>
<text text-anchor="start" x="402.5" y="-296" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white">&lt;</text>
<text text-anchor="start" x="409.5" y="-296" font-family="Roboto" font-weight="bold" font-style="italic" font-size="10.00" fill="white">PolymorphicModel</text>
<text text-anchor="start" x="498.5" y="-296" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white">&gt; &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-280.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-280.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="387.5" y="-280.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-280.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-280.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="517.5" y="-280.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-267.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-267.6" font-family="Roboto" font-weight="bold" font-style="italic" font-size="8.00">polymorphic_ctype</text>
<text text-anchor="start" x="455.5" y="-267.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-267.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-267.6" font-family="Roboto" font-weight="bold" font-style="italic" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="540.5" y="-267.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-254.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-254.6" font-family="Roboto" font-size="8.00">balance</text>
<text text-anchor="start" x="407.5" y="-254.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-254.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-254.6" font-family="Roboto" font-size="8.00">BigIntegerField</text>
<text text-anchor="start" x="533.5" y="-254.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-241.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-241.6" font-family="Roboto" font-size="8.00">created_at</text>
<text text-anchor="start" x="417.5" y="-241.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-241.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-241.6" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="531.5" y="-241.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-228.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-228.6" font-family="Roboto" font-size="8.00">display_image</text>
<text text-anchor="start" x="430.5" y="-228.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-228.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-228.6" font-family="Roboto" font-size="8.00">ImageField</text>
<text text-anchor="start" x="519.5" y="-228.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-215.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-215.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">inactivity_reason</text>
<text text-anchor="start" x="437.5" y="-215.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-215.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-215.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="514.5" y="-215.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-202.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-202.6" font-family="Roboto" font-size="8.00">is_active</text>
<text text-anchor="start" x="410.5" y="-202.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-202.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-202.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="525.5" y="-202.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="369.5" y="-189.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="379.5" y="-189.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">last_negative</text>
<text text-anchor="start" x="426.5" y="-189.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="469.5" y="-189.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="479.5" y="-189.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">DateTimeField</text>
<text text-anchor="start" x="531.5" y="-189.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="366,-183 366,-319 553,-319 553,-183 366,-183"/>
</g>
<!-- note_models_notes_Note&#45;&gt;polymorphic_models_PolymorphicModel -->
<g id="edge2" class="edge">
<title>note_models_notes_Note&#45;&gt;polymorphic_models_PolymorphicModel</title>
<path fill="none" stroke="black" d="M404.81,-178.71C392.51,-162.68 380.13,-146.54 370.04,-133.38"/>
<polygon fill="none" stroke="black" points="372.62,-131 363.76,-125.2 367.07,-135.26 372.62,-131"/>
<text text-anchor="middle" x="410.5" y="-154.6" font-family="Roboto" font-size="8.00"> abstract</text>
<text text-anchor="middle" x="410.5" y="-145.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- note_models_notes_NoteUser -->
<g id="node3" class="node">
<title>note_models_notes_NoteUser</title>
<polygon fill="white" stroke="transparent" points="8,-450.5 8,-499.5 167,-499.5 167,-450.5 8,-450.5"/>
<polygon fill="#1b563f" stroke="transparent" points="9.5,-477 9.5,-498 166.5,-498 166.5,-477 9.5,-477"/>
<text text-anchor="start" x="49" y="-486" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="59" y="-486" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;NoteUser &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">note_ptr</text>
<text text-anchor="start" x="54.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="68.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="154.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21.5" y="-456.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="39.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="68.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="78.5" y="-456.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="154.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="8,-450.5 8,-499.5 167,-499.5 167,-450.5 8,-450.5"/>
</g>
<!-- note_models_notes_NoteUser&#45;&gt;note_models_notes_Note -->
<g id="edge4" class="edge">
<title>note_models_notes_NoteUser&#45;&gt;note_models_notes_Note</title>
<path fill="none" stroke="black" d="M109.11,-446.28C127.2,-424.73 154.77,-395.49 184.5,-377 247.87,-337.59 275.92,-354.68 343.5,-323 345.12,-322.24 346.74,-321.47 348.37,-320.68"/>
<polygon fill="none" stroke="black" points="350.34,-323.6 357.71,-315.99 347.2,-317.34 350.34,-323.6"/>
<text text-anchor="middle" x="311.5" y="-352.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="311.5" y="-343.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- django_contrib_auth_models_User -->
<g id="node14" class="node">
<title>django_contrib_auth_models_User</title>
<polygon fill="white" stroke="transparent" points="65.5,-240.5 65.5,-261.5 109.5,-261.5 109.5,-240.5 65.5,-240.5"/>
<polygon fill="#1b563f" stroke="transparent" points="65.5,-240 65.5,-261 109.5,-261 109.5,-240 65.5,-240"/>
<text text-anchor="start" x="69.5" y="-248.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="74.5" y="-248.4" font-family="Roboto" font-size="12.00" fill="white">User</text>
<text text-anchor="start" x="100.5" y="-248.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- note_models_notes_NoteUser&#45;&gt;django_contrib_auth_models_User -->
<g id="edge3" class="edge">
<title>note_models_notes_NoteUser&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M87.5,-446.33C87.5,-399.84 87.5,-307.38 87.5,-269.15"/>
<text text-anchor="middle" x="109" y="-348.1" font-family="Roboto" font-size="8.00"> user (note)</text>
</g>
<!-- note_models_notes_NoteClub -->
<g id="node4" class="node">
<title>note_models_notes_NoteClub</title>
<polygon fill="white" stroke="transparent" points="1131,-450.5 1131,-499.5 1290,-499.5 1290,-450.5 1131,-450.5"/>
<polygon fill="#1b563f" stroke="transparent" points="1132.5,-477 1132.5,-498 1289.5,-498 1289.5,-477 1132.5,-477"/>
<text text-anchor="start" x="1171.5" y="-486" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1181.5" y="-486" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;NoteClub &#160;&#160;&#160;</text>
<text text-anchor="start" x="1134.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1144.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">note_ptr</text>
<text text-anchor="start" x="1177.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1191.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1201.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="1277.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1134.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1144.5" y="-456.6" font-family="Roboto" font-weight="bold" font-size="8.00">club</text>
<text text-anchor="start" x="1162.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1191.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1201.5" y="-456.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="1277.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="1131,-450.5 1131,-499.5 1290,-499.5 1290,-450.5 1131,-450.5"/>
</g>
<!-- note_models_notes_NoteClub&#45;&gt;note_models_notes_Note -->
<g id="edge6" class="edge">
<title>note_models_notes_NoteClub&#45;&gt;note_models_notes_Note</title>
<path fill="none" stroke="black" d="M1190.62,-446.48C1172.94,-424.15 1145.07,-393.72 1113.5,-377 939.22,-284.69 706.77,-259.9 571.09,-253.63"/>
<polygon fill="none" stroke="black" points="571.23,-250.13 561.09,-253.2 570.92,-257.13 571.23,-250.13"/>
<text text-anchor="middle" x="1084.5" y="-352.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="1084.5" y="-343.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- member_models_Club -->
<g id="node15" class="node">
<title>member_models_Club</title>
<polygon fill="white" stroke="transparent" points="1188.5,-240.5 1188.5,-261.5 1232.5,-261.5 1232.5,-240.5 1188.5,-240.5"/>
<polygon fill="#1b563f" stroke="transparent" points="1188.5,-240 1188.5,-261 1232.5,-261 1232.5,-240 1188.5,-240"/>
<text text-anchor="start" x="1192.5" y="-248.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="1197.5" y="-248.4" font-family="Roboto" font-size="12.00" fill="white">Club</text>
<text text-anchor="start" x="1223.5" y="-248.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- note_models_notes_NoteClub&#45;&gt;member_models_Club -->
<g id="edge5" class="edge">
<title>note_models_notes_NoteClub&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M1210.5,-446.33C1210.5,-399.84 1210.5,-307.38 1210.5,-269.15"/>
<text text-anchor="middle" x="1231" y="-348.1" font-family="Roboto" font-size="8.00"> club (note)</text>
</g>
<!-- note_models_notes_NoteSpecial -->
<g id="node5" class="node">
<title>note_models_notes_NoteSpecial</title>
<polygon fill="white" stroke="transparent" points="926.5,-450.5 926.5,-499.5 1096.5,-499.5 1096.5,-450.5 926.5,-450.5"/>
<polygon fill="#1b563f" stroke="transparent" points="927.5,-477 927.5,-498 1095.5,-498 1095.5,-477 927.5,-477"/>
<text text-anchor="start" x="966" y="-486" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="976" y="-486" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;NoteSpecial &#160;&#160;&#160;</text>
<text text-anchor="start" x="929.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="939.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">note_ptr</text>
<text text-anchor="start" x="972.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="997.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1007.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="1083.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="929.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="939.5" y="-456.6" font-family="Roboto" font-size="8.00">special_type</text>
<text text-anchor="start" x="983.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="997.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1007.5" y="-456.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="1042.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="926.5,-450.5 926.5,-499.5 1096.5,-499.5 1096.5,-450.5 926.5,-450.5"/>
</g>
<!-- note_models_notes_NoteSpecial&#45;&gt;note_models_notes_Note -->
<g id="edge7" class="edge">
<title>note_models_notes_NoteSpecial&#45;&gt;note_models_notes_Note</title>
<path fill="none" stroke="black" d="M989.16,-446.3C970.14,-424.47 940.97,-394.83 909.5,-377 803.05,-316.67 665.72,-284.03 570.92,-267.4"/>
<polygon fill="none" stroke="black" points="571.47,-263.94 561.02,-265.69 570.28,-270.84 571.47,-263.94"/>
<text text-anchor="middle" x="884.5" y="-352.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="884.5" y="-343.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- note_models_notes_Alias -->
<g id="node6" class="node">
<title>note_models_notes_Alias</title>
<polygon fill="white" stroke="transparent" points="717,-437.5 717,-512.5 892,-512.5 892,-437.5 717,-437.5"/>
<polygon fill="#1b563f" stroke="transparent" points="718.5,-490 718.5,-511 891.5,-511 891.5,-490 718.5,-490"/>
<text text-anchor="start" x="775.5" y="-499" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="785.5" y="-499" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Alias &#160;&#160;&#160;</text>
<text text-anchor="start" x="720.5" y="-482.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="730.5" y="-482.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="738.5" y="-482.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="808.5" y="-482.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="818.5" y="-482.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="856.5" y="-482.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="720.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="730.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">note</text>
<text text-anchor="start" x="748.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="808.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="818.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="879.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="720.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="730.5" y="-456.6" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="751.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="808.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="818.5" y="-456.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="853.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="720.5" y="-443.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="730.5" y="-443.6" font-family="Roboto" font-size="8.00">normalized_name</text>
<text text-anchor="start" x="794.5" y="-443.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="808.5" y="-443.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="818.5" y="-443.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="853.5" y="-443.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="717,-437.5 717,-512.5 892,-512.5 892,-437.5 717,-437.5"/>
</g>
<!-- note_models_notes_Alias&#45;&gt;note_models_notes_Note -->
<g id="edge8" class="edge">
<title>note_models_notes_Alias&#45;&gt;note_models_notes_Note</title>
<path fill="none" stroke="black" d="M759.38,-427.39C741.62,-410.4 720.47,-391.75 699.5,-377 656.46,-346.73 605.15,-319.02 561.03,-297.42"/>
<ellipse fill="black" stroke="black" cx="762.4" cy="-430.3" rx="4" ry="4"/>
<text text-anchor="middle" x="691" y="-348.1" font-family="Roboto" font-size="8.00"> note (alias)</text>
</g>
<!-- note_models_transactions_TemplateCategory -->
<g id="node7" class="node">
<title>note_models_transactions_TemplateCategory</title>
<polygon fill="white" stroke="transparent" points="552,-450.5 552,-499.5 683,-499.5 683,-450.5 552,-450.5"/>
<polygon fill="#1b563f" stroke="transparent" points="553.5,-477 553.5,-498 682.5,-498 682.5,-477 553.5,-477"/>
<text text-anchor="start" x="558.5" y="-486" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.5" y="-486" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;TemplateCategory &#160;&#160;&#160;</text>
<text text-anchor="start" x="555.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="565.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="573.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="611.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="621.5" y="-469.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="659.5" y="-469.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="555.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="565.5" y="-456.6" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="586.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="611.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="621.5" y="-456.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="656.5" y="-456.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="552,-450.5 552,-499.5 683,-499.5 683,-450.5 552,-450.5"/>
</g>
<!-- note_models_transactions_TransactionTemplate -->
<g id="node8" class="node">
<title>note_models_transactions_TransactionTemplate</title>
<polygon fill="white" stroke="transparent" points="625,-631 625,-758 806,-758 806,-631 625,-631"/>
<polygon fill="#1b563f" stroke="transparent" points="626.5,-735.5 626.5,-756.5 805.5,-756.5 805.5,-735.5 626.5,-735.5"/>
<text text-anchor="start" x="651" y="-744.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="661" y="-744.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;TransactionTemplate &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-728.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-728.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="646.5" y="-728.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-728.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-728.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="746.5" y="-728.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-715.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-715.1" font-family="Roboto" font-weight="bold" font-size="8.00">category</text>
<text text-anchor="start" x="672.5" y="-715.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-715.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-715.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="769.5" y="-715.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-702.1" font-family="Roboto" font-weight="bold" font-size="8.00">destination</text>
<text text-anchor="start" x="684.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-702.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (note_ptr)</text>
<text text-anchor="start" x="793.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-689.1" font-family="Roboto" font-size="8.00">amount</text>
<text text-anchor="start" x="666.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-689.1" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="778.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-676.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">description</text>
<text text-anchor="start" x="677.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-676.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="743.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-663.1" font-family="Roboto" font-size="8.00">display</text>
<text text-anchor="start" x="663.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-663.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="754.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-650.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-650.1" font-family="Roboto" font-size="8.00">highlighted</text>
<text text-anchor="start" x="677.5" y="-650.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-650.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-650.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="754.5" y="-650.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="628.5" y="-637.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="638.5" y="-637.1" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="659.5" y="-637.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="698.5" y="-637.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="708.5" y="-637.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="743.5" y="-637.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="625,-631 625,-758 806,-758 806,-631 625,-631"/>
</g>
<!-- note_models_transactions_TransactionTemplate&#45;&gt;note_models_notes_NoteClub -->
<g id="edge9" class="edge">
<title>note_models_transactions_TransactionTemplate&#45;&gt;note_models_notes_NoteClub</title>
<path fill="none" stroke="black" d="M822.23,-675.98C905.89,-658.86 1022.9,-627.27 1113.5,-573 1143.74,-554.89 1171.43,-525.3 1189.42,-503.55"/>
<ellipse fill="black" stroke="black" cx="818.16" cy="-676.8" rx="4" ry="4"/>
<text text-anchor="middle" x="1100.5" y="-598.1" font-family="Roboto" font-size="8.00"> destination (+)</text>
</g>
<!-- note_models_transactions_TransactionTemplate&#45;&gt;note_models_transactions_TemplateCategory -->
<g id="edge10" class="edge">
<title>note_models_transactions_TransactionTemplate&#45;&gt;note_models_transactions_TemplateCategory</title>
<path fill="none" stroke="black" d="M682.12,-619.41C664.15,-579.54 643.06,-532.72 629.98,-503.71"/>
<ellipse fill="black" stroke="black" cx="683.79" cy="-623.12" rx="4" ry="4"/>
<text text-anchor="middle" x="713.5" y="-598.1" font-family="Roboto" font-size="8.00"> category (templates)</text>
</g>
<!-- note_models_transactions_Transaction -->
<g id="node9" class="node">
<title>note_models_transactions_Transaction</title>
<polygon fill="white" stroke="transparent" points="201.5,-381 201.5,-569 397.5,-569 397.5,-381 201.5,-381"/>
<polygon fill="#1b563f" stroke="transparent" points="202.5,-538 202.5,-568 396.5,-568 396.5,-538 202.5,-538"/>
<text text-anchor="start" x="261" y="-556" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="271" y="-556" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Transaction</text>
<text text-anchor="start" x="242" y="-546" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white">&lt;</text>
<text text-anchor="start" x="249" y="-546" font-family="Roboto" font-weight="bold" font-style="italic" font-size="10.00" fill="white">PolymorphicModel</text>
<text text-anchor="start" x="338" y="-546" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white">&gt; &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-530.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-530.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="222.5" y="-530.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-530.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-530.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="352.5" y="-530.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-517.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-517.6" font-family="Roboto" font-weight="bold" font-size="8.00">destination</text>
<text text-anchor="start" x="260.5" y="-517.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-517.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-517.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="375.5" y="-517.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-504.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-504.6" font-family="Roboto" font-weight="bold" font-style="italic" font-size="8.00">polymorphic_ctype</text>
<text text-anchor="start" x="290.5" y="-504.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-504.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-504.6" font-family="Roboto" font-weight="bold" font-style="italic" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="375.5" y="-504.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-491.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-491.6" font-family="Roboto" font-weight="bold" font-size="8.00">source</text>
<text text-anchor="start" x="242.5" y="-491.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-491.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-491.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="375.5" y="-491.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-478.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-478.6" font-family="Roboto" font-size="8.00">amount</text>
<text text-anchor="start" x="242.5" y="-478.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-478.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-478.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="384.5" y="-478.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-465.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-465.6" font-family="Roboto" font-size="8.00">created_at</text>
<text text-anchor="start" x="252.5" y="-465.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-465.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-465.6" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="366.5" y="-465.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-452.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-452.6" font-family="Roboto" font-size="8.00">destination_alias</text>
<text text-anchor="start" x="273.5" y="-452.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-452.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-452.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="349.5" y="-452.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-439.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-439.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">invalidity_reason</text>
<text text-anchor="start" x="272.5" y="-439.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-439.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-439.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="349.5" y="-439.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-426.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-426.6" font-family="Roboto" font-size="8.00">quantity</text>
<text text-anchor="start" x="242.5" y="-426.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-426.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-426.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="384.5" y="-426.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-413.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-413.6" font-family="Roboto" font-size="8.00">reason</text>
<text text-anchor="start" x="239.5" y="-413.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-413.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-413.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="349.5" y="-413.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-400.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-400.6" font-family="Roboto" font-size="8.00">source_alias</text>
<text text-anchor="start" x="259.5" y="-400.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-400.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-400.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="349.5" y="-400.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="204.5" y="-387.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214.5" y="-387.6" font-family="Roboto" font-size="8.00">valid</text>
<text text-anchor="start" x="230.5" y="-387.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="304.5" y="-387.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="314.5" y="-387.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="360.5" y="-387.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="201.5,-381 201.5,-569 397.5,-569 397.5,-381 201.5,-381"/>
</g>
<!-- note_models_transactions_Transaction&#45;&gt;polymorphic_models_PolymorphicModel -->
<g id="edge13" class="edge">
<title>note_models_transactions_Transaction&#45;&gt;polymorphic_models_PolymorphicModel</title>
<path fill="none" stroke="black" d="M291.68,-376.8C289.64,-318.52 291.7,-243.46 308.5,-179 312.59,-163.31 320.42,-147.19 327.99,-133.93"/>
<polygon fill="none" stroke="black" points="331.05,-135.64 333.14,-125.26 325.03,-132.07 331.05,-135.64"/>
<text text-anchor="middle" x="328.5" y="-253.6" font-family="Roboto" font-size="8.00"> abstract</text>
<text text-anchor="middle" x="328.5" y="-244.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- note_models_transactions_Transaction&#45;&gt;note_models_notes_Note -->
<g id="edge11" class="edge">
<title>note_models_transactions_Transaction&#45;&gt;note_models_notes_Note</title>
<path fill="none" stroke="black" d="M407.37,-370.47C410.2,-366.68 412.92,-362.85 415.5,-359 422.87,-347.98 429.37,-335.54 434.95,-323.13"/>
<ellipse fill="black" stroke="black" cx="404.93" cy="-373.64" rx="4" ry="4"/>
<text text-anchor="middle" x="445.5" y="-348.1" font-family="Roboto" font-size="8.00"> source (+)</text>
</g>
<!-- note_models_transactions_Transaction&#45;&gt;note_models_notes_Note -->
<g id="edge12" class="edge">
<title>note_models_transactions_Transaction&#45;&gt;note_models_notes_Note</title>
<path fill="none" stroke="black" d="M339.68,-369.46C345.09,-359.49 351.03,-349.83 357.5,-341 361.94,-334.93 366.87,-328.99 372.09,-323.24"/>
<ellipse fill="black" stroke="black" cx="337.7" cy="-373.21" rx="4" ry="4"/>
<text text-anchor="middle" x="384.5" y="-348.1" font-family="Roboto" font-size="8.00"> destination (+)</text>
</g>
<!-- note_models_transactions_RecurrentTransaction -->
<g id="node10" class="node">
<title>note_models_transactions_RecurrentTransaction</title>
<polygon fill="white" stroke="transparent" points="39,-811 39,-860 226,-860 226,-811 39,-811"/>
<polygon fill="#1b563f" stroke="transparent" points="40.5,-837.5 40.5,-858.5 225.5,-858.5 225.5,-837.5 40.5,-837.5"/>
<text text-anchor="start" x="66" y="-846.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="76" y="-846.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;RecurrentTransaction &#160;&#160;&#160;</text>
<text text-anchor="start" x="42.5" y="-830.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="52.5" y="-830.1" font-family="Roboto" font-weight="bold" font-size="8.00">transaction_ptr</text>
<text text-anchor="start" x="113.5" y="-830.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="127.5" y="-830.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="137.5" y="-830.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="213.5" y="-830.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="42.5" y="-817.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="52.5" y="-817.1" font-family="Roboto" font-weight="bold" font-size="8.00">template</text>
<text text-anchor="start" x="86.5" y="-817.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="127.5" y="-817.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="137.5" y="-817.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="198.5" y="-817.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="39,-811 39,-860 226,-860 226,-811 39,-811"/>
</g>
<!-- note_models_transactions_RecurrentTransaction&#45;&gt;note_models_transactions_TransactionTemplate -->
<g id="edge14" class="edge">
<title>note_models_transactions_RecurrentTransaction&#45;&gt;note_models_transactions_TransactionTemplate</title>
<path fill="none" stroke="black" d="M242.27,-830.15C340.58,-823.38 487.34,-806.09 607.5,-762 610.6,-760.86 613.71,-759.63 616.81,-758.32"/>
<ellipse fill="black" stroke="black" cx="238.09" cy="-830.43" rx="4" ry="4"/>
<text text-anchor="middle" x="601.5" y="-782.6" font-family="Roboto" font-size="8.00"> template (recurrenttransaction)</text>
</g>
<!-- note_models_transactions_RecurrentTransaction&#45;&gt;note_models_transactions_Transaction -->
<g id="edge15" class="edge">
<title>note_models_transactions_RecurrentTransaction&#45;&gt;note_models_transactions_Transaction</title>
<path fill="none" stroke="black" d="M124.08,-806.79C113.13,-765.51 98.42,-686.36 125.5,-627 127.55,-622.52 153.83,-599.09 185.37,-571.97"/>
<polygon fill="none" stroke="black" points="187.73,-574.55 193.05,-565.39 183.18,-569.24 187.73,-574.55"/>
<text text-anchor="middle" x="145.5" y="-697.1" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="145.5" y="-688.1" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- note_models_transactions_SpecialTransaction -->
<g id="node11" class="node">
<title>note_models_transactions_SpecialTransaction</title>
<polygon fill="white" stroke="transparent" points="183,-657 183,-732 370,-732 370,-657 183,-657"/>
<polygon fill="#1b563f" stroke="transparent" points="184.5,-709.5 184.5,-730.5 369.5,-730.5 369.5,-709.5 184.5,-709.5"/>
<text text-anchor="start" x="215.5" y="-718.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="225.5" y="-718.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;SpecialTransaction &#160;&#160;&#160;</text>
<text text-anchor="start" x="186.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.5" y="-702.1" font-family="Roboto" font-weight="bold" font-size="8.00">transaction_ptr</text>
<text text-anchor="start" x="257.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="271.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="281.5" y="-702.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="357.5" y="-702.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="186.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.5" y="-689.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">bank</text>
<text text-anchor="start" x="214.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="271.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="281.5" y="-689.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="316.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="186.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.5" y="-676.1" font-family="Roboto" font-size="8.00">first_name</text>
<text text-anchor="start" x="234.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="271.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="281.5" y="-676.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="316.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="186.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.5" y="-663.1" font-family="Roboto" font-size="8.00">last_name</text>
<text text-anchor="start" x="233.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="271.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="281.5" y="-663.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="316.5" y="-663.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="183,-657 183,-732 370,-732 370,-657 183,-657"/>
</g>
<!-- note_models_transactions_SpecialTransaction&#45;&gt;note_models_transactions_Transaction -->
<g id="edge16" class="edge">
<title>note_models_transactions_SpecialTransaction&#45;&gt;note_models_transactions_Transaction</title>
<path fill="none" stroke="black" d="M280.8,-652.85C282.89,-633.07 285.51,-608.27 288.13,-583.55"/>
<polygon fill="none" stroke="black" points="291.63,-583.66 289.21,-573.35 284.67,-582.92 291.63,-583.66"/>
<text text-anchor="middle" x="307.5" y="-602.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="307.5" y="-593.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- note_models_transactions_MembershipTransaction -->
<g id="node12" class="node">
<title>note_models_transactions_MembershipTransaction</title>
<polygon fill="white" stroke="transparent" points="404,-670 404,-719 591,-719 591,-670 404,-670"/>
<polygon fill="#1b563f" stroke="transparent" points="405.5,-696.5 405.5,-717.5 590.5,-717.5 590.5,-696.5 405.5,-696.5"/>
<text text-anchor="start" x="425" y="-705.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="435" y="-705.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;MembershipTransaction &#160;&#160;&#160;</text>
<text text-anchor="start" x="407.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="417.5" y="-689.1" font-family="Roboto" font-weight="bold" font-size="8.00">transaction_ptr</text>
<text text-anchor="start" x="478.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="492.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="502.5" y="-689.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="578.5" y="-689.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="407.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="417.5" y="-676.1" font-family="Roboto" font-weight="bold" font-size="8.00">membership</text>
<text text-anchor="start" x="466.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="492.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="502.5" y="-676.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="578.5" y="-676.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="404,-670 404,-719 591,-719 591,-670 404,-670"/>
</g>
<!-- note_models_transactions_MembershipTransaction&#45;&gt;note_models_transactions_Transaction -->
<g id="edge18" class="edge">
<title>note_models_transactions_MembershipTransaction&#45;&gt;note_models_transactions_Transaction</title>
<path fill="none" stroke="black" d="M472.42,-665.95C452.58,-644.15 423.52,-612.24 394.88,-580.77"/>
<polygon fill="none" stroke="black" points="397.32,-578.25 388,-573.21 392.14,-582.97 397.32,-578.25"/>
<text text-anchor="middle" x="436.5" y="-602.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="436.5" y="-593.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- member_models_Membership -->
<g id="node16" class="node">
<title>member_models_Membership</title>
<polygon fill="white" stroke="transparent" points="431.5,-464.5 431.5,-485.5 517.5,-485.5 517.5,-464.5 431.5,-464.5"/>
<polygon fill="#1b563f" stroke="transparent" points="431.5,-464 431.5,-485 517.5,-485 517.5,-464 431.5,-464"/>
<text text-anchor="start" x="436" y="-472.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="441" y="-472.4" font-family="Roboto" font-size="12.00" fill="white">Membership</text>
<text text-anchor="start" x="508" y="-472.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- note_models_transactions_MembershipTransaction&#45;&gt;member_models_Membership -->
<g id="edge17" class="edge">
<title>note_models_transactions_MembershipTransaction&#45;&gt;member_models_Membership</title>
<path fill="none" stroke="black" d="M494.59,-665.95C489.76,-620.34 480.26,-530.46 476.3,-493.01"/>
<text text-anchor="middle" x="535.5" y="-598.1" font-family="Roboto" font-size="8.00"> membership (transaction)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 55 KiB

171
docs/_static/img/graphs/permission.svg vendored Normal file
View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="352pt" height="373pt"
viewBox="0.00 0.00 352.00 373.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 369)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-369 348,-369 348,4 -4,4"/>
<!-- permission_models_PermissionMask -->
<g id="node1" class="node">
<title>permission_models_PermissionMask</title>
<polygon fill="white" stroke="transparent" points="8,-4 8,-66 186,-66 186,-4 8,-4"/>
<polygon fill="#1b563f" stroke="transparent" points="9,-44 9,-65 185,-65 185,-44 9,-44"/>
<text text-anchor="start" x="41" y="-53" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="51" y="-53" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;PermissionMask &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-36.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="29" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-36.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="122" y="-36.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-23.6" font-family="Roboto" font-size="8.00">description</text>
<text text-anchor="start" x="60" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-23.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="119" y="-23.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-10.6" font-family="Roboto" font-size="8.00">rank</text>
<text text-anchor="start" x="37" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-10.6" font-family="Roboto" font-size="8.00">PositiveSmallIntegerField</text>
<text text-anchor="start" x="173" y="-10.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="8,-4 8,-66 186,-66 186,-4 8,-4"/>
</g>
<!-- permission_models_Permission -->
<g id="node2" class="node">
<title>permission_models_Permission</title>
<polygon fill="white" stroke="transparent" points="104.5,-119 104.5,-246 255.5,-246 255.5,-119 104.5,-119"/>
<polygon fill="#1b563f" stroke="transparent" points="106,-223.5 106,-244.5 255,-244.5 255,-223.5 106,-223.5"/>
<text text-anchor="start" x="136.5" y="-232.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="146.5" y="-232.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Permission &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-216.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="126" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-216.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="220" y="-216.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-203.1" font-family="Roboto" font-weight="bold" font-size="8.00">mask</text>
<text text-anchor="start" x="139" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-203.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="243" y="-203.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-190.1" font-family="Roboto" font-weight="bold" font-size="8.00">model</text>
<text text-anchor="start" x="143" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-190.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="243" y="-190.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-177.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">description</text>
<text text-anchor="start" x="157" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-177.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="217" y="-177.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-164.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">field</text>
<text text-anchor="start" x="133" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-164.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="217" y="-164.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-151.1" font-family="Roboto" font-size="8.00">permanent</text>
<text text-anchor="start" x="158" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-151.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="228" y="-151.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-138.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-138.1" font-family="Roboto" font-size="8.00">query</text>
<text text-anchor="start" x="139" y="-138.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-138.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-138.1" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="214" y="-138.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="108" y="-125.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="118" y="-125.1" font-family="Roboto" font-size="8.00">type</text>
<text text-anchor="start" x="134" y="-125.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="172" y="-125.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="182" y="-125.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="217" y="-125.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="104.5,-119 104.5,-246 255.5,-246 255.5,-119 104.5,-119"/>
</g>
<!-- permission_models_Permission&#45;&gt;permission_models_PermissionMask -->
<g id="edge2" class="edge">
<title>permission_models_Permission&#45;&gt;permission_models_PermissionMask</title>
<path fill="none" stroke="black" d="M137.78,-107.48C130.35,-94.46 122.95,-81.49 116.54,-70.25"/>
<ellipse fill="black" stroke="black" cx="139.92" cy="-111.24" rx="4" ry="4"/>
<text text-anchor="middle" x="168" y="-90.6" font-family="Roboto" font-size="8.00"> mask (permissions)</text>
</g>
<!-- django_contrib_contenttypes_models_ContentType -->
<g id="node4" class="node">
<title>django_contrib_contenttypes_models_ContentType</title>
<polygon fill="white" stroke="transparent" points="220,-24.5 220,-45.5 306,-45.5 306,-24.5 220,-24.5"/>
<polygon fill="#1b563f" stroke="transparent" points="220,-24 220,-45 306,-45 306,-24 220,-24"/>
<text text-anchor="start" x="224" y="-32.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="229" y="-32.4" font-family="Roboto" font-size="12.00" fill="white">ContentType</text>
<text text-anchor="start" x="297" y="-32.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- permission_models_Permission&#45;&gt;django_contrib_contenttypes_models_ContentType -->
<g id="edge1" class="edge">
<title>permission_models_Permission&#45;&gt;django_contrib_contenttypes_models_ContentType</title>
<path fill="none" stroke="black" d="M222.21,-107.51C233.83,-87.14 245.38,-66.89 253.2,-53.17"/>
<ellipse fill="black" stroke="black" cx="220.08" cy="-111.24" rx="4" ry="4"/>
<text text-anchor="middle" x="249.5" y="-90.6" font-family="Roboto" font-size="8.00"> model (+)</text>
</g>
<!-- permission_models_Role -->
<g id="node3" class="node">
<title>permission_models_Role</title>
<polygon fill="white" stroke="transparent" points="173,-299 173,-361 317,-361 317,-299 173,-299"/>
<polygon fill="#1b563f" stroke="transparent" points="174,-339 174,-360 316,-360 316,-339 174,-339"/>
<text text-anchor="start" x="217" y="-348" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="227" y="-348" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Role &#160;&#160;&#160;</text>
<text text-anchor="start" x="176" y="-331.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="186" y="-331.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="194" y="-331.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="233" y="-331.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="243" y="-331.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="281" y="-331.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="176" y="-318.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="186" y="-318.6" font-family="Roboto" font-weight="bold" font-size="8.00">for_club</text>
<text text-anchor="start" x="219" y="-318.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="233" y="-318.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="243" y="-318.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="304" y="-318.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="176" y="-305.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="186" y="-305.6" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="207" y="-305.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="233" y="-305.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="243" y="-305.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="278" y="-305.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="173,-299 173,-361 317,-361 317,-299 173,-299"/>
</g>
<!-- permission_models_Role&#45;&gt;permission_models_Permission -->
<g id="edge4" class="edge">
<title>permission_models_Role&#45;&gt;permission_models_Permission</title>
<path fill="none" stroke="black" d="M226.45,-287.47C222.27,-278.13 217.73,-267.95 213.16,-257.73"/>
<ellipse fill="black" stroke="black" cx="228.13" cy="-291.23" rx="4" ry="4"/>
<ellipse fill="black" stroke="black" cx="211.47" cy="-253.94" rx="4" ry="4"/>
<text text-anchor="middle" x="254" y="-270.6" font-family="Roboto" font-size="8.00"> permissions (role)</text>
</g>
<!-- member_models_Club -->
<g id="node5" class="node">
<title>member_models_Club</title>
<polygon fill="white" stroke="transparent" points="289,-172 289,-193 333,-193 333,-172 289,-172"/>
<polygon fill="#1b563f" stroke="transparent" points="289,-171.5 289,-192.5 333,-192.5 333,-171.5 289,-171.5"/>
<text text-anchor="start" x="293" y="-179.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="298" y="-179.9" font-family="Roboto" font-size="12.00" fill="white">Club</text>
<text text-anchor="start" x="324" y="-179.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- permission_models_Role&#45;&gt;member_models_Club -->
<g id="edge3" class="edge">
<title>permission_models_Role&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M282.52,-288.02C284.92,-284.43 287.12,-280.73 289,-277 301.53,-252.13 307.07,-219.92 309.41,-200.53"/>
<ellipse fill="black" stroke="black" cx="280.03" cy="-291.53" rx="4" ry="4"/>
<text text-anchor="middle" x="318" y="-270.6" font-family="Roboto" font-size="8.00"> for_club (role)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

324
docs/_static/img/graphs/treasury.svg vendored Normal file
View File

@ -0,0 +1,324 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="970pt" height="493pt"
viewBox="0.00 0.00 970.00 493.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 489)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-489 966,-489 966,4 -4,4"/>
<!-- treasury_models_Invoice -->
<g id="node1" class="node">
<title>treasury_models_Invoice</title>
<polygon fill="white" stroke="transparent" points="8,-187 8,-340 176,-340 176,-187 8,-187"/>
<polygon fill="#1b563f" stroke="transparent" points="9,-317.5 9,-338.5 175,-338.5 175,-317.5 9,-317.5"/>
<text text-anchor="start" x="57.5" y="-326.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="67.5" y="-326.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Invoice &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-310.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-310.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="29" y="-310.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-310.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-310.1" font-family="Roboto" font-weight="bold" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="163" y="-310.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-297.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-297.1" font-family="Roboto" font-size="8.00">acquitted</text>
<text text-anchor="start" x="54" y="-297.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-297.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-297.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="130" y="-297.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-284.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-284.1" font-family="Roboto" font-size="8.00">address</text>
<text text-anchor="start" x="49" y="-284.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-284.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-284.1" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="116" y="-284.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-271.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-271.1" font-family="Roboto" font-size="8.00">bde</text>
<text text-anchor="start" x="35" y="-271.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-271.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-271.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="119" y="-271.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-258.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-258.1" font-family="Roboto" font-size="8.00">date</text>
<text text-anchor="start" x="37" y="-258.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-258.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-258.1" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="118" y="-258.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-245.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-245.1" font-family="Roboto" font-size="8.00">description</text>
<text text-anchor="start" x="60" y="-245.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-245.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-245.1" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="116" y="-245.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-232.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-232.1" font-family="Roboto" font-size="8.00">locked</text>
<text text-anchor="start" x="44" y="-232.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-232.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-232.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="130" y="-232.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-219.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-219.1" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="42" y="-219.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-219.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-219.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="119" y="-219.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-206.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-206.1" font-family="Roboto" font-size="8.00">object</text>
<text text-anchor="start" x="43" y="-206.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-206.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-206.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="119" y="-206.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="11" y="-193.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="21" y="-193.1" font-family="Roboto" font-size="8.00">tex</text>
<text text-anchor="start" x="32" y="-193.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="74" y="-193.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="84" y="-193.1" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="116" y="-193.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="8,-187 8,-340 176,-340 176,-187 8,-187"/>
</g>
<!-- treasury_models_Product -->
<g id="node2" class="node">
<title>treasury_models_Product</title>
<polygon fill="white" stroke="transparent" points="11.5,-393 11.5,-481 172.5,-481 172.5,-393 11.5,-393"/>
<polygon fill="#1b563f" stroke="transparent" points="13,-459 13,-480 172,-480 172,-459 13,-459"/>
<text text-anchor="start" x="57" y="-468" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="67" y="-468" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Product &#160;&#160;&#160;</text>
<text text-anchor="start" x="15" y="-451.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="25" y="-451.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="33" y="-451.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="80" y="-451.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="90" y="-451.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="128" y="-451.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="15" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="25" y="-438.6" font-family="Roboto" font-weight="bold" font-size="8.00">invoice</text>
<text text-anchor="start" x="53" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="80" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="90" y="-438.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="151" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="15" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="25" y="-425.6" font-family="Roboto" font-size="8.00">amount</text>
<text text-anchor="start" x="53" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="80" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="90" y="-425.6" font-family="Roboto" font-size="8.00">IntegerField</text>
<text text-anchor="start" x="133" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="15" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="25" y="-412.6" font-family="Roboto" font-size="8.00">designation</text>
<text text-anchor="start" x="66" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="80" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="90" y="-412.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="125" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="15" y="-399.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="25" y="-399.6" font-family="Roboto" font-size="8.00">quantity</text>
<text text-anchor="start" x="53" y="-399.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="80" y="-399.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="90" y="-399.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="160" y="-399.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="11.5,-393 11.5,-481 172.5,-481 172.5,-393 11.5,-393"/>
</g>
<!-- treasury_models_Product&#45;&gt;treasury_models_Invoice -->
<g id="edge1" class="edge">
<title>treasury_models_Product&#45;&gt;treasury_models_Invoice</title>
<path fill="none" stroke="black" d="M92,-380.43C92,-368.88 92,-356.48 92,-344.25"/>
<ellipse fill="black" stroke="black" cx="92" cy="-384.68" rx="4" ry="4"/>
<text text-anchor="middle" x="124.5" y="-364.6" font-family="Roboto" font-size="8.00"> invoice (products)</text>
</g>
<!-- treasury_models_RemittanceType -->
<g id="node3" class="node">
<title>treasury_models_RemittanceType</title>
<polygon fill="white" stroke="transparent" points="213.5,-85 213.5,-134 382.5,-134 382.5,-85 213.5,-85"/>
<polygon fill="#1b563f" stroke="transparent" points="215,-111.5 215,-132.5 382,-132.5 382,-111.5 215,-111.5"/>
<text text-anchor="start" x="243" y="-120.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="253" y="-120.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;RemittanceType &#160;&#160;&#160;</text>
<text text-anchor="start" x="217" y="-104.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="227" y="-104.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="235" y="-104.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="259" y="-104.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="269" y="-104.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="307" y="-104.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="217" y="-91.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="227" y="-91.1" font-family="Roboto" font-weight="bold" font-size="8.00">note</text>
<text text-anchor="start" x="245" y="-91.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="259" y="-91.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="269" y="-91.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (note_ptr)</text>
<text text-anchor="start" x="370" y="-91.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="213.5,-85 213.5,-134 382.5,-134 382.5,-85 213.5,-85"/>
</g>
<!-- note_models_notes_NoteSpecial -->
<g id="node7" class="node">
<title>note_models_notes_NoteSpecial</title>
<polygon fill="white" stroke="transparent" points="255,-7.5 255,-28.5 341,-28.5 341,-7.5 255,-7.5"/>
<polygon fill="#1b563f" stroke="transparent" points="255,-7 255,-28 341,-28 341,-7 255,-7"/>
<text text-anchor="start" x="259.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="264.5" y="-15.4" font-family="Roboto" font-size="12.00" fill="white">NoteSpecial</text>
<text text-anchor="start" x="331.5" y="-15.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- treasury_models_RemittanceType&#45;&gt;note_models_notes_NoteSpecial -->
<g id="edge2" class="edge">
<title>treasury_models_RemittanceType&#45;&gt;note_models_notes_NoteSpecial</title>
<path fill="none" stroke="black" d="M298,-80.67C298,-66.1 298,-48.72 298,-36.13"/>
<text text-anchor="middle" x="337.5" y="-56.6" font-family="Roboto" font-size="8.00"> note (remittancetype)</text>
</g>
<!-- treasury_models_Remittance -->
<g id="node4" class="node">
<title>treasury_models_Remittance</title>
<polygon fill="white" stroke="transparent" points="210.5,-219.5 210.5,-307.5 385.5,-307.5 385.5,-219.5 210.5,-219.5"/>
<polygon fill="#1b563f" stroke="transparent" points="212,-285.5 212,-306.5 385,-306.5 385,-285.5 212,-285.5"/>
<text text-anchor="start" x="254" y="-294.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="264" y="-294.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Remittance &#160;&#160;&#160;</text>
<text text-anchor="start" x="214" y="-278.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="224" y="-278.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="232" y="-278.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="302" y="-278.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="312" y="-278.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="350" y="-278.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214" y="-265.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="224" y="-265.1" font-family="Roboto" font-weight="bold" font-size="8.00">remittance_type</text>
<text text-anchor="start" x="288" y="-265.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="302" y="-265.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="312" y="-265.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="373" y="-265.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214" y="-252.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="224" y="-252.1" font-family="Roboto" font-size="8.00">closed</text>
<text text-anchor="start" x="247" y="-252.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="302" y="-252.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="312" y="-252.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="358" y="-252.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214" y="-239.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="224" y="-239.1" font-family="Roboto" font-size="8.00">comment</text>
<text text-anchor="start" x="258" y="-239.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="302" y="-239.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="312" y="-239.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="347" y="-239.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="214" y="-226.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="224" y="-226.1" font-family="Roboto" font-size="8.00">date</text>
<text text-anchor="start" x="240" y="-226.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="302" y="-226.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="312" y="-226.1" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="364" y="-226.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="210.5,-219.5 210.5,-307.5 385.5,-307.5 385.5,-219.5 210.5,-219.5"/>
</g>
<!-- treasury_models_Remittance&#45;&gt;treasury_models_RemittanceType -->
<g id="edge3" class="edge">
<title>treasury_models_Remittance&#45;&gt;treasury_models_RemittanceType</title>
<path fill="none" stroke="black" d="M298,-207.09C298,-183.68 298,-157.46 298,-138.24"/>
<ellipse fill="black" stroke="black" cx="298" cy="-211.24" rx="4" ry="4"/>
<text text-anchor="middle" x="350" y="-158.6" font-family="Roboto" font-size="8.00"> remittance_type (remittance)</text>
</g>
<!-- treasury_models_SpecialTransactionProxy -->
<g id="node5" class="node">
<title>treasury_models_SpecialTransactionProxy</title>
<polygon fill="white" stroke="transparent" points="272.5,-406 272.5,-468 497.5,-468 497.5,-406 272.5,-406"/>
<polygon fill="#1b563f" stroke="transparent" points="274,-446 274,-467 497,-467 497,-446 274,-446"/>
<text text-anchor="start" x="310" y="-455" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="320" y="-455" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;SpecialTransactionProxy &#160;&#160;&#160;</text>
<text text-anchor="start" x="276" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="286" y="-438.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="294" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="346" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="356" y="-438.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="394" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="276" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="286" y="-425.6" font-family="Roboto" font-weight="bold" font-size="8.00">remittance</text>
<text text-anchor="start" x="328" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="346" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="356" y="-425.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="417" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="276" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="286" y="-412.6" font-family="Roboto" font-weight="bold" font-size="8.00">transaction</text>
<text text-anchor="start" x="332" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="346" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="356" y="-412.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (transaction_ptr)</text>
<text text-anchor="start" x="485" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="272.5,-406 272.5,-468 497.5,-468 497.5,-406 272.5,-406"/>
</g>
<!-- treasury_models_SpecialTransactionProxy&#45;&gt;treasury_models_Remittance -->
<g id="edge5" class="edge">
<title>treasury_models_SpecialTransactionProxy&#45;&gt;treasury_models_Remittance</title>
<path fill="none" stroke="black" d="M318.41,-396.34C310.62,-388.91 303.76,-380.46 299,-371 289.94,-353 288.41,-331.02 289.64,-311.71"/>
<ellipse fill="black" stroke="black" cx="321.56" cy="-399.15" rx="4" ry="4"/>
<text text-anchor="middle" x="364" y="-364.6" font-family="Roboto" font-size="8.00"> remittance (specialtransactionproxy)</text>
</g>
<!-- note_models_transactions_SpecialTransaction -->
<g id="node8" class="node">
<title>note_models_transactions_SpecialTransaction</title>
<polygon fill="white" stroke="transparent" points="483,-253 483,-274 605,-274 605,-253 483,-253"/>
<polygon fill="#1b563f" stroke="transparent" points="483,-252.5 483,-273.5 605,-273.5 605,-252.5 483,-252.5"/>
<text text-anchor="start" x="487.5" y="-260.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="492.5" y="-260.9" font-family="Roboto" font-size="12.00" fill="white">SpecialTransaction</text>
<text text-anchor="start" x="595.5" y="-260.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- treasury_models_SpecialTransactionProxy&#45;&gt;note_models_transactions_SpecialTransaction -->
<g id="edge4" class="edge">
<title>treasury_models_SpecialTransactionProxy&#45;&gt;note_models_transactions_SpecialTransaction</title>
<path fill="none" stroke="black" d="M415.54,-401.97C426.93,-389.37 440,-374.98 452,-362 478.22,-333.63 509.12,-301.06 527.61,-281.66"/>
<text text-anchor="middle" x="518" y="-364.6" font-family="Roboto" font-size="8.00"> transaction (specialtransactionproxy)</text>
</g>
<!-- treasury_models_SogeCredit -->
<g id="node6" class="node">
<title>treasury_models_SogeCredit</title>
<polygon fill="white" stroke="transparent" points="612,-406 612,-468 864,-468 864,-406 612,-406"/>
<polygon fill="#1b563f" stroke="transparent" points="613,-446 613,-467 863,-467 863,-446 613,-446"/>
<text text-anchor="start" x="694.5" y="-455" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="704.5" y="-455" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;SogeCredit &#160;&#160;&#160;</text>
<text text-anchor="start" x="615" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="625" y="-438.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="633" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="712" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="722" y="-438.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="760" y="-438.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="615" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="625" y="-425.6" font-family="Roboto" font-weight="bold" font-size="8.00">credit_transaction</text>
<text text-anchor="start" x="698" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="712" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="722" y="-425.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (transaction_ptr)</text>
<text text-anchor="start" x="851" y="-425.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="615" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="625" y="-412.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="643" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="712" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="722" y="-412.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="798" y="-412.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="612,-406 612,-468 864,-468 864,-406 612,-406"/>
</g>
<!-- treasury_models_SogeCredit&#45;&gt;note_models_transactions_SpecialTransaction -->
<g id="edge7" class="edge">
<title>treasury_models_SogeCredit&#45;&gt;note_models_transactions_SpecialTransaction</title>
<path fill="none" stroke="black" d="M668.71,-401.98C653.52,-392.97 638.09,-382.51 625,-371 594.47,-344.16 567.83,-304.24 554.03,-281.68"/>
<text text-anchor="middle" x="679.5" y="-364.6" font-family="Roboto" font-size="8.00"> credit_transaction (sogecredit)</text>
</g>
<!-- django_contrib_auth_models_User -->
<g id="node9" class="node">
<title>django_contrib_auth_models_User</title>
<polygon fill="white" stroke="transparent" points="716,-253 716,-274 760,-274 760,-253 716,-253"/>
<polygon fill="#1b563f" stroke="transparent" points="716,-252.5 716,-273.5 760,-273.5 760,-252.5 716,-252.5"/>
<text text-anchor="start" x="720" y="-260.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="725" y="-260.9" font-family="Roboto" font-size="12.00" fill="white">User</text>
<text text-anchor="start" x="751" y="-260.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- treasury_models_SogeCredit&#45;&gt;django_contrib_auth_models_User -->
<g id="edge6" class="edge">
<title>treasury_models_SogeCredit&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M738,-401.71C738,-365.34 738,-309.39 738,-281.51"/>
<text text-anchor="middle" x="769.5" y="-364.6" font-family="Roboto" font-size="8.00"> user (sogecredit)</text>
</g>
<!-- note_models_transactions_MembershipTransaction -->
<g id="node10" class="node">
<title>note_models_transactions_MembershipTransaction</title>
<polygon fill="white" stroke="transparent" points="794,-253 794,-274 940,-274 940,-253 794,-253"/>
<polygon fill="#1b563f" stroke="transparent" points="794,-252.5 794,-273.5 940,-273.5 940,-252.5 794,-252.5"/>
<text text-anchor="start" x="798" y="-260.9" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="803" y="-260.9" font-family="Roboto" font-size="12.00" fill="white">MembershipTransaction</text>
<text text-anchor="start" x="931" y="-260.9" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- treasury_models_SogeCredit&#45;&gt;note_models_transactions_MembershipTransaction -->
<g id="edge8" class="edge">
<title>treasury_models_SogeCredit&#45;&gt;note_models_transactions_MembershipTransaction</title>
<path fill="none" stroke="black" d="M782.72,-395.99C790.55,-388.06 798.32,-379.54 805,-371 825.4,-344.92 843.79,-311.46 855.15,-289.06"/>
<ellipse fill="black" stroke="black" cx="779.57" cy="-399.11" rx="4" ry="4"/>
<ellipse fill="black" stroke="black" cx="857.08" cy="-285.21" rx="4" ry="4"/>
<text text-anchor="middle" x="886.5" y="-364.6" font-family="Roboto" font-size="8.00"> transactions (_sogecredit_transactions_+)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 29 KiB

603
docs/_static/img/graphs/wei.svg vendored Normal file
View File

@ -0,0 +1,603 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (0)
-->
<!-- Title: model_graph Pages: 1 -->
<svg width="1207pt" height="864pt"
viewBox="0.00 0.00 1206.76 864.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 860)">
<title>model_graph</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-860 1202.76,-860 1202.76,4 -4,4"/>
<!-- wei_models_WEIClub -->
<g id="node1" class="node">
<title>wei_models_WEIClub</title>
<polygon fill="white" stroke="transparent" points="357.76,-219 357.76,-294 520.76,-294 520.76,-219 357.76,-219"/>
<polygon fill="#1b563f" stroke="transparent" points="359.26,-271.5 359.26,-292.5 520.26,-292.5 520.26,-271.5 359.26,-271.5"/>
<text text-anchor="start" x="401.76" y="-280.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="411.76" y="-280.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;WEIClub &#160;&#160;&#160;</text>
<text text-anchor="start" x="361.26" y="-264.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="371.26" y="-264.1" font-family="Roboto" font-weight="bold" font-size="8.00">club_ptr</text>
<text text-anchor="start" x="404.26" y="-264.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="422.26" y="-264.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="432.26" y="-264.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="508.26" y="-264.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="361.26" y="-251.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="371.26" y="-251.1" font-family="Roboto" font-size="8.00">date_end</text>
<text text-anchor="start" x="405.26" y="-251.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="422.26" y="-251.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="432.26" y="-251.1" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="466.26" y="-251.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="361.26" y="-238.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="371.26" y="-238.1" font-family="Roboto" font-size="8.00">date_start</text>
<text text-anchor="start" x="408.26" y="-238.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="422.26" y="-238.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="432.26" y="-238.1" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="466.26" y="-238.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="361.26" y="-225.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="371.26" y="-225.1" font-family="Roboto" font-size="8.00">year</text>
<text text-anchor="start" x="387.26" y="-225.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="422.26" y="-225.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="432.26" y="-225.1" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="502.26" y="-225.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="357.76,-219 357.76,-294 520.76,-294 520.76,-219 357.76,-219"/>
</g>
<!-- member_models_Club -->
<g id="node8" class="node">
<title>member_models_Club</title>
<polygon fill="white" stroke="transparent" points="554.76,-4 554.76,-157 763.76,-157 763.76,-4 554.76,-4"/>
<polygon fill="#1b563f" stroke="transparent" points="556.26,-134.5 556.26,-155.5 763.26,-155.5 763.26,-134.5 556.26,-134.5"/>
<text text-anchor="start" x="631.26" y="-143.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="641.26" y="-143.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Club &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-127.1" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="576.26" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-127.1" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="719.26" y="-127.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-114.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">parent_club</text>
<text text-anchor="start" x="616.26" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-114.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">ForeignKey (id)</text>
<text text-anchor="start" x="742.26" y="-114.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-101.1" font-family="Roboto" font-size="8.00">email</text>
<text text-anchor="start" x="587.26" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-101.1" font-family="Roboto" font-size="8.00">EmailField</text>
<text text-anchor="start" x="718.26" y="-101.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-88.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">membership_duration</text>
<text text-anchor="start" x="647.26" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-88.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">PositiveIntegerField</text>
<text text-anchor="start" x="751.26" y="-88.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-75.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">membership_end</text>
<text text-anchor="start" x="631.26" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-75.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">DateField</text>
<text text-anchor="start" x="715.26" y="-75.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-62.1" font-family="Roboto" font-size="8.00">membership_fee_paid</text>
<text text-anchor="start" x="648.26" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-62.1" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="751.26" y="-62.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-49.1" font-family="Roboto" font-size="8.00">membership_fee_unpaid</text>
<text text-anchor="start" x="657.26" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-49.1" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="751.26" y="-49.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-36.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">membership_start</text>
<text text-anchor="start" x="633.26" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-36.1" font-family="Roboto" font-size="8.00" fill="#7b7b7b">DateField</text>
<text text-anchor="start" x="715.26" y="-36.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-23.1" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="589.26" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-23.1" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="716.26" y="-23.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="558.26" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="568.26" y="-10.1" font-family="Roboto" font-size="8.00">require_memberships</text>
<text text-anchor="start" x="645.26" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="671.26" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="681.26" y="-10.1" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="727.26" y="-10.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="554.76,-4 554.76,-157 763.76,-157 763.76,-4 554.76,-4"/>
</g>
<!-- wei_models_WEIClub&#45;&gt;member_models_Club -->
<g id="edge1" class="edge">
<title>wei_models_WEIClub&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M490.56,-214.92C508.57,-200.68 529.56,-184.08 550.48,-167.53"/>
<polygon fill="none" stroke="black" points="552.82,-170.14 558.49,-161.2 548.48,-164.65 552.82,-170.14"/>
<text text-anchor="middle" x="550.26" y="-190.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="550.26" y="-181.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- wei_models_Bus -->
<g id="node2" class="node">
<title>wei_models_Bus</title>
<polygon fill="white" stroke="transparent" points="112.26,-347 112.26,-435 306.26,-435 306.26,-347 112.26,-347"/>
<polygon fill="#1b563f" stroke="transparent" points="113.26,-413 113.26,-434 305.26,-434 305.26,-413 113.26,-413"/>
<text text-anchor="start" x="182.76" y="-422" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="192.76" y="-422" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Bus &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.26" y="-405.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.26" y="-405.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="133.26" y="-405.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="198.26" y="-405.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="208.26" y="-405.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="246.26" y="-405.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.26" y="-392.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.26" y="-392.6" font-family="Roboto" font-weight="bold" font-size="8.00">wei</text>
<text text-anchor="start" x="138.26" y="-392.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="198.26" y="-392.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="208.26" y="-392.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (club_ptr)</text>
<text text-anchor="start" x="293.26" y="-392.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.26" y="-379.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.26" y="-379.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">description</text>
<text text-anchor="start" x="164.26" y="-379.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="198.26" y="-379.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="208.26" y="-379.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">TextField</text>
<text text-anchor="start" x="240.26" y="-379.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.26" y="-366.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.26" y="-366.6" font-family="Roboto" font-size="8.00">information_json</text>
<text text-anchor="start" x="184.26" y="-366.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="198.26" y="-366.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="208.26" y="-366.6" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="240.26" y="-366.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="115.26" y="-353.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="125.26" y="-353.6" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="146.26" y="-353.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="198.26" y="-353.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="208.26" y="-353.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="243.26" y="-353.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="112.26,-347 112.26,-435 306.26,-435 306.26,-347 112.26,-347"/>
</g>
<!-- wei_models_Bus&#45;&gt;wei_models_WEIClub -->
<g id="edge2" class="edge">
<title>wei_models_Bus&#45;&gt;wei_models_WEIClub</title>
<path fill="none" stroke="black" d="M298.21,-338.76C321.62,-325.27 346.55,-310.91 368.84,-298.07"/>
<ellipse fill="black" stroke="black" cx="294.46" cy="-340.92" rx="4" ry="4"/>
<text text-anchor="middle" x="353.26" y="-318.6" font-family="Roboto" font-size="8.00"> wei (buses)</text>
</g>
<!-- wei_models_BusTeam -->
<g id="node3" class="node">
<title>wei_models_BusTeam</title>
<polygon fill="white" stroke="transparent" points="129.76,-562 129.76,-650 288.76,-650 288.76,-562 129.76,-562"/>
<polygon fill="#1b563f" stroke="transparent" points="131.26,-628 131.26,-649 288.26,-649 288.26,-628 131.26,-628"/>
<text text-anchor="start" x="170.76" y="-637" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="180.76" y="-637" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;BusTeam &#160;&#160;&#160;</text>
<text text-anchor="start" x="133.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="143.26" y="-620.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="151.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="206.26" y="-620.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="244.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="133.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="143.26" y="-607.6" font-family="Roboto" font-weight="bold" font-size="8.00">bus</text>
<text text-anchor="start" x="159.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="206.26" y="-607.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="267.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="133.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="143.26" y="-594.6" font-family="Roboto" font-size="8.00">color</text>
<text text-anchor="start" x="161.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="206.26" y="-594.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="276.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="133.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="143.26" y="-581.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">description</text>
<text text-anchor="start" x="182.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="206.26" y="-581.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">TextField</text>
<text text-anchor="start" x="238.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="133.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="143.26" y="-568.6" font-family="Roboto" font-size="8.00">name</text>
<text text-anchor="start" x="164.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="196.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="206.26" y="-568.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="241.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="129.76,-562 129.76,-650 288.76,-650 288.76,-562 129.76,-562"/>
</g>
<!-- wei_models_BusTeam&#45;&gt;wei_models_Bus -->
<g id="edge3" class="edge">
<title>wei_models_BusTeam&#45;&gt;wei_models_Bus</title>
<path fill="none" stroke="black" d="M209.26,-549.93C209.26,-515.64 209.26,-471.98 209.26,-439.26"/>
<ellipse fill="black" stroke="black" cx="209.26" cy="-553.99" rx="4" ry="4"/>
<text text-anchor="middle" x="232.26" y="-464.1" font-family="Roboto" font-size="8.00"> bus (teams)</text>
</g>
<!-- wei_models_WEIRole -->
<g id="node4" class="node">
<title>wei_models_WEIRole</title>
<polygon fill="white" stroke="transparent" points="589.76,-588 589.76,-624 746.76,-624 746.76,-588 589.76,-588"/>
<polygon fill="#1b563f" stroke="transparent" points="591.26,-602 591.26,-623 746.26,-623 746.26,-602 591.26,-602"/>
<text text-anchor="start" x="631.26" y="-611" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="641.26" y="-611" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;WEIRole &#160;&#160;&#160;</text>
<text text-anchor="start" x="593.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="603.26" y="-594.6" font-family="Roboto" font-weight="bold" font-size="8.00">role_ptr</text>
<text text-anchor="start" x="634.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="648.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="658.26" y="-594.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="734.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="589.76,-588 589.76,-624 746.76,-624 746.76,-588 589.76,-588"/>
</g>
<!-- permission_models_Role -->
<g id="node10" class="node">
<title>permission_models_Role</title>
<polygon fill="white" stroke="transparent" points="667.26,-380.5 667.26,-401.5 711.26,-401.5 711.26,-380.5 667.26,-380.5"/>
<polygon fill="#1b563f" stroke="transparent" points="667.26,-380 667.26,-401 711.26,-401 711.26,-380 667.26,-380"/>
<text text-anchor="start" x="671.76" y="-388.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="676.76" y="-388.4" font-family="Roboto" font-size="12.00" fill="white">Role</text>
<text text-anchor="start" x="701.76" y="-388.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- wei_models_WEIRole&#45;&gt;permission_models_Role -->
<g id="edge4" class="edge">
<title>wei_models_WEIRole&#45;&gt;permission_models_Role</title>
<path fill="none" stroke="black" d="M663.01,-583.63C656.73,-554.58 648.26,-501.21 658.26,-457 661.3,-443.57 667.46,-429.66 673.5,-418.17"/>
<polygon fill="none" stroke="black" points="676.71,-419.6 678.47,-409.16 670.57,-416.23 676.71,-419.6"/>
<text text-anchor="middle" x="678.26" y="-468.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="678.26" y="-459.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- wei_models_WEIRegistration -->
<g id="node5" class="node">
<title>wei_models_WEIRegistration</title>
<polygon fill="white" stroke="transparent" points="323.26,-503.5 323.26,-708.5 555.26,-708.5 555.26,-503.5 323.26,-503.5"/>
<polygon fill="#1b563f" stroke="transparent" points="324.26,-686 324.26,-707 554.26,-707 554.26,-686 324.26,-686"/>
<text text-anchor="start" x="384.26" y="-695" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="394.26" y="-695" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;WEIRegistration &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-678.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-678.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="344.26" y="-678.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-678.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-678.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="495.26" y="-678.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-665.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-665.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="354.26" y="-665.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-665.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-665.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="518.26" y="-665.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-652.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-652.6" font-family="Roboto" font-weight="bold" font-size="8.00">wei</text>
<text text-anchor="start" x="349.26" y="-652.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-652.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-652.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (club_ptr)</text>
<text text-anchor="start" x="542.26" y="-652.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-639.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-639.6" font-family="Roboto" font-size="8.00">birth_date</text>
<text text-anchor="start" x="373.26" y="-639.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-639.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-639.6" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="491.26" y="-639.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-626.6" font-family="Roboto" font-size="8.00">caution_check</text>
<text text-anchor="start" x="387.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-626.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="503.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-613.6" font-family="Roboto" font-size="8.00">clothing_cut</text>
<text text-anchor="start" x="379.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-613.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="492.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-600.6" font-family="Roboto" font-size="8.00">clothing_size</text>
<text text-anchor="start" x="382.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-600.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="492.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-587.6" font-family="Roboto" font-size="8.00">emergency_contact_name</text>
<text text-anchor="start" x="431.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-587.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="492.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-574.6" font-family="Roboto" font-size="8.00">emergency_contact_phone</text>
<text text-anchor="start" x="433.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-574.6" font-family="Roboto" font-size="8.00">PhoneNumberField</text>
<text text-anchor="start" x="527.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-561.6" font-family="Roboto" font-size="8.00">first_year</text>
<text text-anchor="start" x="370.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-561.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="503.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-548.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-548.6" font-family="Roboto" font-size="8.00">gender</text>
<text text-anchor="start" x="362.26" y="-548.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-548.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-548.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="492.26" y="-548.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-535.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-535.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">health_issues</text>
<text text-anchor="start" x="385.26" y="-535.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-535.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-535.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">TextField</text>
<text text-anchor="start" x="489.26" y="-535.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-522.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-522.6" font-family="Roboto" font-size="8.00">information_json</text>
<text text-anchor="start" x="395.26" y="-522.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-522.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-522.6" font-family="Roboto" font-size="8.00">TextField</text>
<text text-anchor="start" x="489.26" y="-522.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="326.26" y="-509.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="336.26" y="-509.6" font-family="Roboto" font-size="8.00">soge_credit</text>
<text text-anchor="start" x="377.26" y="-509.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="447.26" y="-509.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="457.26" y="-509.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="503.26" y="-509.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="323.26,-503.5 323.26,-708.5 555.26,-708.5 555.26,-503.5 323.26,-503.5"/>
</g>
<!-- wei_models_WEIRegistration&#45;&gt;wei_models_WEIClub -->
<g id="edge6" class="edge">
<title>wei_models_WEIRegistration&#45;&gt;wei_models_WEIClub</title>
<path fill="none" stroke="black" d="M439.26,-491.29C439.26,-424.89 439.26,-345.15 439.26,-298.05"/>
<ellipse fill="black" stroke="black" cx="439.26" cy="-495.32" rx="4" ry="4"/>
<text text-anchor="middle" x="460.76" y="-389.1" font-family="Roboto" font-size="8.00"> wei (users)</text>
</g>
<!-- django_contrib_auth_models_User -->
<g id="node11" class="node">
<title>django_contrib_auth_models_User</title>
<polygon fill="white" stroke="transparent" points="905.26,-380.5 905.26,-401.5 949.26,-401.5 949.26,-380.5 905.26,-380.5"/>
<polygon fill="#1b563f" stroke="transparent" points="905.26,-380 905.26,-401 949.26,-401 949.26,-380 905.26,-380"/>
<text text-anchor="start" x="909.26" y="-388.4" font-family="Roboto" font-size="8.00"> &#160;</text>
<text text-anchor="start" x="914.26" y="-388.4" font-family="Roboto" font-size="12.00" fill="white">User</text>
<text text-anchor="start" x="940.26" y="-388.4" font-family="Roboto" font-size="8.00"> &#160;</text>
</g>
<!-- wei_models_WEIRegistration&#45;&gt;django_contrib_auth_models_User -->
<g id="edge5" class="edge">
<title>wei_models_WEIRegistration&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M565.81,-495.81C567.95,-494.83 570.1,-493.89 572.26,-493 663.38,-455.48 699.65,-505.8 793.26,-475 837.19,-460.54 881.52,-429 906.57,-409.23"/>
<ellipse fill="black" stroke="black" cx="562.18" cy="-497.57" rx="4" ry="4"/>
<text text-anchor="middle" x="852.76" y="-464.1" font-family="Roboto" font-size="8.00"> user (wei)</text>
</g>
<!-- wei_models_WEIMembership -->
<g id="node6" class="node">
<title>wei_models_WEIMembership</title>
<polygon fill="white" stroke="transparent" points="196.26,-777 196.26,-852 386.26,-852 386.26,-777 196.26,-777"/>
<polygon fill="#1b563f" stroke="transparent" points="197.26,-829.5 197.26,-850.5 385.26,-850.5 385.26,-829.5 197.26,-829.5"/>
<text text-anchor="start" x="235.76" y="-838.5" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="245.76" y="-838.5" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;WEIMembership &#160;&#160;&#160;</text>
<text text-anchor="start" x="199.26" y="-822.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="209.26" y="-822.1" font-family="Roboto" font-weight="bold" font-size="8.00">membership_ptr</text>
<text text-anchor="start" x="273.26" y="-822.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287.26" y="-822.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297.26" y="-822.1" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="373.26" y="-822.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="199.26" y="-809.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="209.26" y="-809.1" font-family="Roboto" font-weight="bold" font-size="8.00">bus</text>
<text text-anchor="start" x="225.26" y="-809.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287.26" y="-809.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297.26" y="-809.1" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="358.26" y="-809.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="199.26" y="-796.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="209.26" y="-796.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">registration</text>
<text text-anchor="start" x="255.26" y="-796.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287.26" y="-796.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297.26" y="-796.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">OneToOneField (id)</text>
<text text-anchor="start" x="373.26" y="-796.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="199.26" y="-783.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="209.26" y="-783.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">team</text>
<text text-anchor="start" x="228.26" y="-783.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="287.26" y="-783.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="297.26" y="-783.1" font-family="Roboto" font-weight="bold" font-size="8.00" fill="#7b7b7b">ForeignKey (id)</text>
<text text-anchor="start" x="358.26" y="-783.1" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="196.26,-777 196.26,-852 386.26,-852 386.26,-777 196.26,-777"/>
</g>
<!-- wei_models_WEIMembership&#45;&gt;wei_models_Bus -->
<g id="edge7" class="edge">
<title>wei_models_WEIMembership&#45;&gt;wei_models_Bus</title>
<path fill="none" stroke="black" d="M180.12,-800.54C128.6,-788.33 71.89,-764.71 40.26,-719 -16.9,-636.4 -9.31,-580.36 40.26,-493 54.76,-467.43 78.6,-447.65 103.85,-432.64"/>
<ellipse fill="black" stroke="black" cx="184.11" cy="-801.44" rx="4" ry="4"/>
<text text-anchor="middle" x="76.26" y="-604.1" font-family="Roboto" font-size="8.00"> bus (memberships)</text>
</g>
<!-- wei_models_WEIMembership&#45;&gt;wei_models_BusTeam -->
<g id="edge8" class="edge">
<title>wei_models_WEIMembership&#45;&gt;wei_models_BusTeam</title>
<path fill="none" stroke="black" d="M271.95,-764.88C258.68,-731.47 241.16,-687.33 228.01,-654.22"/>
<ellipse fill="black" stroke="black" cx="273.58" cy="-768.98" rx="4" ry="4"/>
<text text-anchor="middle" x="305.76" y="-744.1" font-family="Roboto" font-size="8.00"> team (memberships)</text>
</g>
<!-- wei_models_WEIMembership&#45;&gt;wei_models_WEIRegistration -->
<g id="edge9" class="edge">
<title>wei_models_WEIMembership&#45;&gt;wei_models_WEIRegistration</title>
<path fill="none" stroke="black" d="M332.93,-772.99C338.3,-767.14 343.55,-761.04 348.26,-755 358.55,-741.79 368.65,-727.36 378.17,-712.85"/>
<text text-anchor="middle" x="407.26" y="-744.1" font-family="Roboto" font-size="8.00"> registration (membership)</text>
</g>
<!-- member_models_Membership -->
<g id="node9" class="node">
<title>member_models_Membership</title>
<polygon fill="white" stroke="transparent" points="780.76,-555.5 780.76,-656.5 937.76,-656.5 937.76,-555.5 780.76,-555.5"/>
<polygon fill="#1b563f" stroke="transparent" points="782.26,-634 782.26,-655 937.26,-655 937.26,-634 782.26,-634"/>
<text text-anchor="start" x="813.76" y="-643" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="823.76" y="-643" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Membership &#160;&#160;&#160;</text>
<text text-anchor="start" x="784.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="794.26" y="-626.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="802.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="845.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="855.26" y="-626.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="893.26" y="-626.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="784.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="794.26" y="-613.6" font-family="Roboto" font-weight="bold" font-size="8.00">club</text>
<text text-anchor="start" x="812.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="845.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="855.26" y="-613.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="916.26" y="-613.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="784.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="794.26" y="-600.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="812.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="845.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="855.26" y="-600.6" font-family="Roboto" font-weight="bold" font-size="8.00">ForeignKey (id)</text>
<text text-anchor="start" x="916.26" y="-600.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="784.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="794.26" y="-587.6" font-family="Roboto" font-size="8.00">date_end</text>
<text text-anchor="start" x="828.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="845.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="855.26" y="-587.6" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="889.26" y="-587.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="784.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="794.26" y="-574.6" font-family="Roboto" font-size="8.00">date_start</text>
<text text-anchor="start" x="831.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="845.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="855.26" y="-574.6" font-family="Roboto" font-size="8.00">DateField</text>
<text text-anchor="start" x="889.26" y="-574.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="784.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="794.26" y="-561.6" font-family="Roboto" font-size="8.00">fee</text>
<text text-anchor="start" x="806.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="845.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="855.26" y="-561.6" font-family="Roboto" font-size="8.00">PositiveIntegerField</text>
<text text-anchor="start" x="925.26" y="-561.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="780.76,-555.5 780.76,-656.5 937.76,-656.5 937.76,-555.5 780.76,-555.5"/>
</g>
<!-- wei_models_WEIMembership&#45;&gt;member_models_Membership -->
<g id="edge10" class="edge">
<title>wei_models_WEIMembership&#45;&gt;member_models_Membership</title>
<path fill="none" stroke="black" d="M394.29,-811.98C494.78,-806.29 648.78,-786.06 763.26,-719 784.62,-706.48 803.55,-687.45 818.82,-668.6"/>
<polygon fill="none" stroke="black" points="821.72,-670.58 825.14,-660.55 816.21,-666.25 821.72,-670.58"/>
<text text-anchor="middle" x="749.26" y="-748.6" font-family="Roboto" font-size="8.00"> multi&#45;table</text>
<text text-anchor="middle" x="749.26" y="-739.6" font-family="Roboto" font-size="8.00">inheritance</text>
</g>
<!-- member_models_Profile -->
<g id="node7" class="node">
<title>member_models_Profile</title>
<polygon fill="white" stroke="transparent" points="971.76,-497 971.76,-715 1190.76,-715 1190.76,-497 971.76,-497"/>
<polygon fill="#1b563f" stroke="transparent" points="973.26,-693 973.26,-714 1190.26,-714 1190.26,-693 973.26,-693"/>
<text text-anchor="start" x="1049.26" y="-702" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1059.26" y="-702" font-family="Roboto" font-weight="bold" font-size="10.00" fill="white"> &#160;&#160;&#160;Profile &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-685.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-685.6" font-family="Roboto" font-weight="bold" font-size="8.00">id</text>
<text text-anchor="start" x="993.26" y="-685.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-685.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-685.6" font-family="Roboto" font-weight="bold" font-size="8.00">AutoField</text>
<text text-anchor="start" x="1127.26" y="-685.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-672.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-672.6" font-family="Roboto" font-weight="bold" font-size="8.00">user</text>
<text text-anchor="start" x="1003.26" y="-672.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-672.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-672.6" font-family="Roboto" font-weight="bold" font-size="8.00">OneToOneField (id)</text>
<text text-anchor="start" x="1165.26" y="-672.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-659.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-659.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">address</text>
<text text-anchor="start" x="1013.26" y="-659.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-659.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-659.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="1124.26" y="-659.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-646.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-646.6" font-family="Roboto" font-size="8.00">department</text>
<text text-anchor="start" x="1027.26" y="-646.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-646.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-646.6" font-family="Roboto" font-size="8.00">CharField</text>
<text text-anchor="start" x="1124.26" y="-646.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-633.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-633.6" font-family="Roboto" font-size="8.00">email_confirmed</text>
<text text-anchor="start" x="1044.26" y="-633.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-633.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-633.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="1135.26" y="-633.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-620.6" font-family="Roboto" font-size="8.00">last_report</text>
<text text-anchor="start" x="1023.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-620.6" font-family="Roboto" font-size="8.00">DateTimeField</text>
<text text-anchor="start" x="1141.26" y="-620.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-607.6" font-family="Roboto" font-size="8.00">ml_art_registration</text>
<text text-anchor="start" x="1052.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-607.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="1135.26" y="-607.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-594.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">ml_events_registration</text>
<text text-anchor="start" x="1065.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-594.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="1124.26" y="-594.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-581.6" font-family="Roboto" font-size="8.00">ml_sport_registration</text>
<text text-anchor="start" x="1060.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-581.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="1135.26" y="-581.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-568.6" font-family="Roboto" font-size="8.00">paid</text>
<text text-anchor="start" x="1001.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-568.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="1135.26" y="-568.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-555.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-555.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">phone_number</text>
<text text-anchor="start" x="1040.26" y="-555.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-555.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-555.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">PhoneNumberField</text>
<text text-anchor="start" x="1159.26" y="-555.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-542.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-542.6" font-family="Roboto" font-size="8.00">promotion</text>
<text text-anchor="start" x="1021.26" y="-542.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-542.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-542.6" font-family="Roboto" font-size="8.00">PositiveSmallIntegerField</text>
<text text-anchor="start" x="1178.26" y="-542.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-529.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-529.6" font-family="Roboto" font-size="8.00">registration_valid</text>
<text text-anchor="start" x="1045.26" y="-529.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-529.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-529.6" font-family="Roboto" font-size="8.00">BooleanField</text>
<text text-anchor="start" x="1135.26" y="-529.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-516.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-516.6" font-family="Roboto" font-size="8.00">report_frequency</text>
<text text-anchor="start" x="1046.26" y="-516.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-516.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-516.6" font-family="Roboto" font-size="8.00">PositiveSmallIntegerField</text>
<text text-anchor="start" x="1178.26" y="-516.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="975.26" y="-503.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="985.26" y="-503.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">section</text>
<text text-anchor="start" x="1010.26" y="-503.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1079.26" y="-503.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<text text-anchor="start" x="1089.26" y="-503.6" font-family="Roboto" font-size="8.00" fill="#7b7b7b">CharField</text>
<text text-anchor="start" x="1124.26" y="-503.6" font-family="Roboto" font-size="8.00"> &#160;&#160;&#160;</text>
<polygon fill="none" stroke="black" points="971.76,-497 971.76,-715 1190.76,-715 1190.76,-497 971.76,-497"/>
</g>
<!-- member_models_Profile&#45;&gt;django_contrib_auth_models_User -->
<g id="edge11" class="edge">
<title>member_models_Profile&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M1010.97,-492.62C1002.57,-480.41 993.9,-468.32 985.26,-457 972.23,-439.93 955.81,-421.8 943.77,-409.03"/>
<text text-anchor="middle" x="1020.26" y="-464.1" font-family="Roboto" font-size="8.00"> user (profile)</text>
</g>
<!-- member_models_Club&#45;&gt;member_models_Club -->
<g id="edge12" class="edge">
<title>member_models_Club&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M779.77,-93.53C786.05,-90.32 789.76,-85.97 789.76,-80.5 789.76,-73.05 782.88,-67.69 772.02,-64.43"/>
<ellipse fill="black" stroke="black" cx="775.74" cy="-95.11" rx="4" ry="4"/>
<text text-anchor="middle" x="823.26" y="-78.6" font-family="Roboto" font-size="8.00"> parent_club (club)</text>
</g>
<!-- member_models_Membership&#45;&gt;member_models_Club -->
<g id="edge14" class="edge">
<title>member_models_Membership&#45;&gt;member_models_Club</title>
<path fill="none" stroke="black" d="M863.76,-543.26C864.15,-516.37 862.46,-484.73 855.26,-457 826.89,-347.74 762.07,-234.51 714.72,-161.34"/>
<ellipse fill="black" stroke="black" cx="863.67" cy="-547.45" rx="4" ry="4"/>
<text text-anchor="middle" x="841.26" y="-318.6" font-family="Roboto" font-size="8.00"> club (membership)</text>
</g>
<!-- member_models_Membership&#45;&gt;permission_models_Role -->
<g id="edge15" class="edge">
<title>member_models_Membership&#45;&gt;permission_models_Role</title>
<path fill="none" stroke="black" d="M816.49,-545.05C801.23,-526.63 782.92,-507.44 763.26,-493 746.89,-480.98 735.35,-489.62 721.26,-475 705.92,-459.09 697.73,-435.03 693.49,-416.91"/>
<ellipse fill="black" stroke="black" cx="819.01" cy="-548.15" rx="4" ry="4"/>
<ellipse fill="black" stroke="black" cx="692.65" cy="-412.96" rx="4" ry="4"/>
<text text-anchor="middle" x="757.26" y="-464.1" font-family="Roboto" font-size="8.00"> roles (membership)</text>
</g>
<!-- member_models_Membership&#45;&gt;django_contrib_auth_models_User -->
<g id="edge13" class="edge">
<title>member_models_Membership&#45;&gt;django_contrib_auth_models_User</title>
<path fill="none" stroke="black" d="M878.91,-543.46C893.51,-497.7 912.45,-438.39 921.69,-409.44"/>
<ellipse fill="black" stroke="black" cx="877.63" cy="-547.44" rx="4" ry="4"/>
<text text-anchor="middle" x="943.76" y="-464.1" font-family="Roboto" font-size="8.00"> user (memberships)</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

370
docs/api/activity.rst Normal file
View File

@ -0,0 +1,370 @@
API Activités
=============
Activité
--------
**Chemin :** `/api/activity/activity/ <https://note.crans.org/api/activity/activity/>`_
Options
~~~~~~~
.. code:: json
{
"name": "Activity List",
"description": "REST API View set.\nThe djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,\nthen render it on /api/activity/activity/",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"name": {
"type": "string",
"required": true,
"read_only": false,
"label": "Nom",
"max_length": 255
},
"description": {
"type": "string",
"required": true,
"read_only": false,
"label": "Description"
},
"location": {
"type": "string",
"required": false,
"read_only": false,
"label": "Lieu",
"help_text": "Lieu o\u00f9 l'activit\u00e9 est organis\u00e9e, par exemple la Kfet.",
"max_length": 255
},
"date_start": {
"type": "datetime",
"required": true,
"read_only": false,
"label": "Date de d\u00e9but"
},
"date_end": {
"type": "datetime",
"required": true,
"read_only": false,
"label": "Date de fin"
},
"valid": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "Valide"
},
"open": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "Ouvrir"
},
"activity_type": {
"type": "field",
"required": true,
"read_only": false,
"label": "Type"
},
"creater": {
"type": "field",
"required": true,
"read_only": false,
"label": "Utilisateur"
},
"organizer": {
"type": "field",
"required": true,
"read_only": false,
"label": "Organisateur",
"help_text": "Le club qui organise l'activit\u00e9. Les co\u00fbts d'invitation iront pour ce club."
},
"attendees_club": {
"type": "field",
"required": true,
"read_only": false,
"label": "Club attendu",
"help_text": "Club qui est autoris\u00e9 \u00e0 rejoindre l'activit\u00e9. Tr\u00e8s souvent le club Kfet."
}
}
}
}
Filtres Django
~~~~~~~~~~~~~~
* ``name``
* ``description``
* ``activity_type``
* ``location``
* ``creater``
* ``organizer``
* ``attendees_club``
* ``date_start``
* ``date_end``
* ``valid``
* ``open``
Filtres de recherche
~~~~~~~~~~~~~~~~~~~~
* ``name`` (expression régulière)
* ``description`` (expression régulière)
* ``location`` (expression régulière)
* ``creater__last_name`` (expression régulière)
* ``creater__first_name`` (expression régulière)
* ``creater__email`` (expression régulière)
* ``creater__note__alias__name`` (expression régulière)
* ``creater__note__alias__normalized_name`` (expression régulière)
* ``organizer__name`` (expression régulière)
* ``organizer__email`` (expression régulière)
* ``organizer__note__alias__name`` (expression régulière)
* ``organizer__note__alias__normalized_name`` (expression régulière)
* ``attendees_club__name`` (expression régulière)
* ``attendees_club__email`` (expression régulière)
* ``attendees_club__note__alias__name`` (expression régulière)
* ``attendees_club__note__alias__normalized_name`` (expression régulière)
Type d'activité
---------------
**Chemin :** `/api/activity/type/ <https://note.crans.org/api/activity/type/>`_
Options
~~~~~~~
.. code:: json
{
"name": "Activity Type List",
"description": "REST API View set.\nThe djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,\nthen render it on /api/activity/type/",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"name": {
"type": "string",
"required": true,
"read_only": false,
"label": "Nom",
"max_length": 255
},
"manage_entries": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "G\u00e9rer les entr\u00e9es",
"help_text": "Activer le support des entr\u00e9es pour cette activit\u00e9."
},
"can_invite": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "Peut inviter"
},
"guest_entry_fee": {
"type": "integer",
"required": false,
"read_only": false,
"label": "Cotisation de l'entr\u00e9e invit\u00e9",
"min_value": 0,
"max_value": 2147483647
}
}
}
}
Filtres Django
~~~~~~~~~~~~~~
* ``name``
* ``manage_entries``
* ``can_invite``
* ``guest_entry_fee``
Invité
------
**Chemin :** `/api/activity/guest/ <https://note.crans.org/api/activity/guest/>`_
Options
~~~~~~~
.. code:: json
{
"name": "Guest List",
"description": "REST API View set.\nThe djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,\nthen render it on /api/activity/guest/",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"last_name": {
"type": "string",
"required": true,
"read_only": false,
"label": "Nom de famille",
"max_length": 255
},
"first_name": {
"type": "string",
"required": true,
"read_only": false,
"label": "Pr\u00e9nom",
"max_length": 255
},
"activity": {
"type": "field",
"required": true,
"read_only": false,
"label": "Activity"
},
"inviter": {
"type": "field",
"required": true,
"read_only": false,
"label": "H\u00f4te"
}
}
}
}
Filtres Django
~~~~~~~~~~~~~~
* ``activity``
* ``activity__name``
* ``last_name``
* ``first_name``
* ``inviter``
* ``inviter__alias__name``
* ``inviter__alias__normalized_name``
Filtres de recherche
~~~~~~~~~~~~~~~~~~~~
* ``activity__name`` (expression régulière)
* ``last_name`` (expression régulière)
* ``first_name`` (expression régulière)
* ``inviter__user__email`` (expression régulière)
* ``inviter__alias__name`` (expression régulière)
* ``inviter__alias__normalized_name`` (expression régulière)
Entrée
------
**Chemin :** `/api/activity/entry/ <https://note.crans.org/api/activity/entry/>`_
Options
~~~~~~~
.. code:: json
{
"name": "Entry List",
"description": "REST API View set.\nThe djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer,\nthen render it on /api/activity/entry/",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"time": {
"type": "datetime",
"required": false,
"read_only": false,
"label": "Heure d'entr\u00e9e"
},
"activity": {
"type": "field",
"required": true,
"read_only": false,
"label": "Activit\u00e9"
},
"note": {
"type": "field",
"required": true,
"read_only": false,
"label": "Note"
},
"guest": {
"type": "field",
"required": true,
"read_only": false,
"label": "Guest"
}
}
}
}
Filtres Django
~~~~~~~~~~~~~~
* ``activity``
* ``time``
* ``note``
* ``guest``
Filtres de recherche
~~~~~~~~~~~~~~~~~~~~
* ``activity__name`` (expression régulière)
* ``note__user__email`` (expression régulière)
* ``note__alias__name`` (expression régulière)
* ``note__alias__normalized_name`` (expression régulière)
* ``guest__last_name`` (expression régulière)
* ``guest__first_name`` (expression régulière)

157
docs/api/basic.rst Normal file
View File

@ -0,0 +1,157 @@
API générale
============
Utilisateur
-----------
**Chemin :** `/api/user/ <https://note.crans.org/api/user/>`_
Options
~~~~~~~
.. code:: json
{
"name": "User List",
"description": "REST API View set.\nThe djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,\nthen render it on /api/user/",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"POST": {
"id": {
"type": "integer",
"required": false,
"read_only": true,
"label": "ID"
},
"last_login": {
"type": "datetime",
"required": false,
"read_only": false,
"label": "Derni\u00e8re connexion"
},
"is_superuser": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "Statut super-utilisateur",
"help_text": "Pr\u00e9cise que l'utilisateur poss\u00e8de toutes les permissions sans les assigner explicitement."
},
"username": {
"type": "string",
"required": true,
"read_only": false,
"label": "Pseudo",
"help_text": "Requis. 150 caract\u00e8res maximum. Uniquement des lettres, nombres et les caract\u00e8res \u00ab\u00a0@\u00a0\u00bb, \u00ab\u00a0.\u00a0\u00bb, \u00ab\u00a0+\u00a0\u00bb, \u00ab\u00a0-\u00a0\u00bb et \u00ab\u00a0_\u00a0\u00bb.",
"max_length": 150
},
"first_name": {
"type": "string",
"required": false,
"read_only": false,
"label": "Pr\u00e9nom",
"max_length": 30
},
"last_name": {
"type": "string",
"required": false,
"read_only": false,
"label": "Nom de famille",
"max_length": 150
},
"email": {
"type": "email",
"required": false,
"read_only": false,
"label": "Adresse \u00e9lectronique",
"max_length": 254
},
"is_staff": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "Statut \u00e9quipe",
"help_text": "Pr\u00e9cise si l'utilisateur peut se connecter \u00e0 ce site d'administration."
},
"is_active": {
"type": "boolean",
"required": false,
"read_only": false,
"label": "Actif",
"help_text": "Pr\u00e9cise si l'utilisateur doit \u00eatre consid\u00e9r\u00e9 comme actif. D\u00e9cochez ceci plut\u00f4t que de supprimer le compte."
},
"date_joined": {
"type": "datetime",
"required": false,
"read_only": false,
"label": "Date d'inscription"
}
}
}
}
Filtres Django
~~~~~~~~~~~~~~
* ``id``
* ``username``
* ``first_name``
* ``last_name``
* ``email``
* ``is_superuser``
* ``is_staff``
* ``is_active``
* ``note__alias__name``
* ``note__alias__normalized_name``
Filtres de recherche
~~~~~~~~~~~~~~~~~~~~
* ``note__alias`` (expression régulière, cherche en priorité les alias les plus proches, puis cherche les alias normalisés)
* ``last_name`` (expression régulière)
* ``first_name`` (expression régulière)
Type de contenu
---------------
**Chemin :** `/api/models/ <https://note.crans.org/api/models/>`_
Options
~~~~~~~
.. code:: json
{
"name": "Content Type List",
"description": "REST API View set.\nThe djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,\nthen render it on /api/models/",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
]
}
Filtres Django
~~~~~~~~~~~~~~
* ``id``
* ``app_label``
* ``model``
Filtres de recherche
~~~~~~~~~~~~~~~~~~~~
* ``app_label`` (expression régulière)
* ``model`` (expression régulière)

214
docs/api/index.rst Normal file
View File

@ -0,0 +1,214 @@
API
===
.. toctree::
:maxdepth: 2
:caption: Applications
activity
basic
logs
member
note
permission
treasury
wei
La NoteKfet2020 dispose d'une API REST. Elle est accessible sur `/api/ <https://note.crans.org/api/>`_.
Elle supporte les requêtes GET, POST, HEAD, PUT, PATCH et DELETE (peut varier selon les pages).
Pages de l'API
--------------
Il suffit d'ajouter le préfixe ``/api/`` pour arriver sur ces pages.
* `models <basic#type-de-contenu>`_ : liste des différents modèles enregistrés en base de données
* `user <basic#utilisateur>`_ : liste des différents utilisateurs enregistrés
* `members/profile <member#profil-utilisateur>`_ : liste des différents profils associés à des utilisateurs
* `members/club <member#club>`_ : liste des différents clubs enregistrés
* `members/membership <member#adhesion>`_ : liste des adhésions enregistrées
* `activity/activity <activity#activite>`_ : liste des activités recensées
* `activity/type <activity#type-d-activite>`_ : liste des différents types d'activités : pots, soirées de club, ...
* `activity/guest <activity#invite>`_ : liste des personnes invitées lors d'une activité
* `activity/entry <activity#entree>`_ : liste des entrées effectuées lors des activités
* `note/note <note#note>`_ : liste des notes enregistrées
* `note/alias <note#alias>`_ : liste des alias enregistrés
* `note/consumer <note#consommateur>`_ : liste des alias enregistrés avec leur note associée
* `note/transaction/category <note#categorie-de-transaction>`_ : liste des différentes catégories de boutons : soft, alcool, ...
* `note/transaction/transaction <note#transaction>`_ : liste des transactions effectuées
* `note/transaction/template <note#modele-de-transaction>`_ : liste des boutons enregistrés
* `treasury/invoice <treasury#facture>`_ : liste des factures générées
* `treasury/product <treasury#produit>`_ : liste des produits associés à des factures
* `treasury/remittance_type <treasury#type-de-remise>`_ : liste des types de remises supportés : chèque
* `treasury/remittance <treasury#remise>`_ : liste des différentes remises enregistrées
* `treasury/remittance <treasury#remise>`_ : liste des crédits de la Société générale enregistrés
* `permission/permission <permission#permission>`_ : liste de toutes les permissions enregistrées
* `permission/roles <permission#permissions-par-roles>`_ : liste des permissions octroyées pour chacun des rôles
* `logs <logs#journal-de-modification>`_ : liste des modifications enregistrées en base de données
* `wei/club <wei#wei>`_ : liste des WEI
* `wei/bus <wei#bus>`_ : liste des bus de tous les WEI
* `wei/team <wei#equipe-de-bus>`_ : liste des équipes de tous les WEI
* `wei/role <wei#role-au-wei>`_ : liste des rôles possibles pour le WEI
* `wei/registration <wei#participation-au-wei>`_ : liste de toutes les inscriptions à un WEI
* `wei/membership <wei#adhesion-au-wei>`_ : liste des adhésions compètes à un WEI
Utilisation de l'API
--------------------
La page ``/api/<model>/`` affiche la liste de tous les éléments enregistrés. La page ``/api/<model>/<pk>/`` affiche
les attributs d'un objet uniquement.
L'affichage des données peut se faire sous deux formes : via une interface HTML propre ou directement en affichant
le JSON brut. Le changement peut se faire en ajoutant en paramètre de l'URL ``format=json`` ou ``format=api``, ou bien
en plaçant en en-tête de la requête ``Accept: application/json`` ou ``Accept: text/html``.
L'API Web propose des formulaires facilitant l'ajout et la modification d'éléments.
S'authentifier
~~~~~~~~~~~~~~
L'authentification peut se faire soit par session en se connectant via la page de connexion classique,
soit via un jeton d'authentification. Le jeton peut se récupérer via la page de son propre compte, en cliquant
sur le bouton « `Accès API <https://note.crans.org/accounts/manage-auth-token/>`_ ». Il peut être révoqué et regénéré
en un clic.
Pour s'authentifier via ce jeton, il faut ajouter l'en-tête ``Authorization: Token <TOKEN>`` aux paramètres HTTP.
En s'authentifiant par cette méthode, les masques de droit sont ignorés, les droits maximaux sont accordés.
GET
~~~
Une requête GET affiche un ou des éléments. Si on veut la liste de tous les éléments d'un modèle, la réponse
est de cette forme :
.. code:: json
{
"count": "<COUNT>",
"next": "/api/<MODEL>/?page=<NEXT_PAGE>",
"previous": "/api/<MODEL>/?page=<NEXT_PAGE>",
"results": [ ]
}
``<COUNT>`` est le nombre d'éléments trouvés. La page n'affiche les informations que 20 par 20 pour ne pas
augmenter inutilement la taille de la réponse. Les champs ``next`` et ``previous`` contiennent les URL des pages
suivantes et précédentes (``null`` si première ou dernière page). Le champ ``results`` contient enfin l'ensemble des
objets trouvés, au format JSON.
Certaines pages disposent de filtres, permettant de sélectionner les objets recherchés. Par exemple, il est possible
de chercher une note d'un certain type matchant avec un certain alias.
Trois types de filtres sont implémentés :
* Les filtres Django, permettant d'ajouter ``?key=value`` dans l'URL pour filtrer les objets ayant ``value`` comme
valeur pour la clé ``key`` ;
* Les filtres de recherche, permettant une recherche plus souple notamment par expressions régulières ou contenance,
et permet aussi de chercher parmi plusieurs clés à partir d'un champ ``search`` dans l'URL ;
* Les filtres de tri, qui ne filtrent pas réellement mais changent l'ordre. En ajoutant ``?ordering=key`` dans l'URL,
on trie les résultats selon la clé ``key`` dans l'ordre croissant, et ``?ordering=-key`` trie dans l'ordre
décroissant.
Les filtres disponibles sont indiqués sur chacune des pages de documentation.
Le résultat est déjà par défaut filtré par droits : seuls les éléments que l'utilisateur à le droit de voir sont affichés.
Cela est possible grâce à la structure des permissions, générant justement des filtres de requêtes de base de données.
Une requête à l'adresse ``/api/<model>/<pk>/`` affiche directement les informations du modèle demandé au format JSON.
POST
~~~~
Une requête POST permet d'ajouter des éléments. Cette requête n'est possible que sur la page ``/api/<model>/``,
la requête POST n'est pas supportée sur les pages de détails (car cette requête permet ... l'ajout).
Des exceptions sont faites sur certaines pages : les pages de logs et de contenttypes sont en lecture uniquement.
Les formats supportés sont multiples : ``application/json``, ``application/x-www-url-encoded``, ``multipart/form-data``.
Cela facilite l'envoi de requêtes. Le module construit ensuite l'instance du modèle et le sauvegarde dans la base de
données. L'application ``permission`` s'assure que l'utilisateur à le droit de faire ce type de modification. La réponse
renvoyée est l'objet enregistré au format JSON si l'ajout s'est bien déroulé, sinon un message d'erreur au format JSON.
PATCH
~~~~~
Une requête PATCH permet de modifier un élément. Ce type de requête n'est disponible que sur la page de détails d'un
élément : ``/api/<model>/pk/``.
Comme pour la requête POST, les formats supportés sont multiples : ``application/json``,
``application/x-www-url-encoded``, ``multipart/form-data``.
Il n'est pas utile d'indiquer tous les champs du modèle : seuls ceux qui sont à modifier suffisent.
Attention : pour les modèles polymorphiques (``Note``, ``Transaction``), il faut toujours spécifier le type de modèle
pour que l'API arrive à s'y retrouver. Par exemple, si on veut rendre la transaction n°42 non valide, on effectue une
requête ``PATCH`` sur ``/api/note/transaction/transaction/42/`` avec les données suivantes :
.. code:: json
{
"valid": false,
"resourcetype": "RecurrentTransaction"
}
PUT
~~~
Une requête PUT permet de remplacer un élément. Ce type de requête n'est disponible que sur la page de détails d'un
élément : ``/api/<model>/pk/``.
Comme pour les requêtes POST ou PATCH, les formats supportés sont multiples : ``application/json``,
``application/x-www-url-encoded``, ``multipart/form-data``.
Contrairement à la requête PATCH, l'intégralité du modèle est remplacé. L'ancien modèle est détruit, on en recrée un
nouveau avec la même clé primaire. Le fonctionnement est similaire à une requête POST.
DELETE
~~~~~~
Une requête de type DELETE permet de supprimer un élément. Ce type de requête n'est disponible que sur la page de
détails d'un élément : ``/api/<model>/pk/``.
Aucune donnée n'est nécessaire. Le module de permissions vérifiera que la suppression est possible. Une erreur
est sinon renvoyée.
OPTIONS
~~~~~~~
Une reqête OPTIONS affiche l'ensemble des opérations possibles sur un modèle ou une instance. Prototype d'une réponse :
.. code:: json
{
"name": "<NAME>",
"description": "<DESCRIPTION>",
"renders": [
"application/json",
"text/html"
],
"parses": [
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"actions": {
"<METHOD>": {
"<FIELD_NAME>": {
"type": "<TYPE>",
"required": "<REQUIRED>",
"read_only": "<READ_ONLY>",
"label": "<LABEL>"
}
}
}
}
* ``<METHOD>`` est le type de requête HTTP supporté (pour modification, inclus dans {``POST``, ``PUT``, ``PATCH``}).
* ``<FIELD_NAME>`` est le nom du champ dans le modèle concerné (exemple : ``id``)
* ``<TYPE>`` représente le type de données : ``integer``, ``string``, ``date``, ``choice``, ``field`` (pour les clés étrangères), ...
* ``<REQUIRED>`` est un booléen indiquant si le champ est requis dans le modèle ou s'il peut être nul/vide.
* ``<READ_ONLY>`` est un booléen indiquant si le champ est accessible en lecture uniquement.
* ``<LABEL>`` représente le label du champ, son nom traduit, qui s'affiche dans le formulaire accessible sur l'API Web.
Des contraintes peuvent s'ajouter à cela selon les champs : taille maximale de chaînes de caractères, valeurs minimales
et maximales pour les entiers ...

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