forked from mirrors/bookwyrm
Compare commits
2272 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6f1d5da43d | ||
|
16f5832540 | ||
|
491fac2938 | ||
|
b68d32d4e2 | ||
|
0de7815b4c | ||
|
ff12102d6f | ||
|
5e655980e7 | ||
|
b9b94a814f | ||
|
d71ee1f012 | ||
|
cad9a76c8a | ||
|
1c9e8aee73 | ||
|
f9b3ed57ee | ||
|
46cef3a16f | ||
|
371f6f0051 | ||
|
8bebfd369a | ||
|
668a36a749 | ||
|
a3ec61a8a8 | ||
|
7ce531f568 | ||
|
86b31f26e7 | ||
|
7080732064 | ||
|
0f0c7a704e | ||
|
83bbdeba1b | ||
|
937cc79234 | ||
|
69242e7ebc | ||
|
14a0ba92ba | ||
|
92a311e766 | ||
|
5c42cfd5eb | ||
|
9afa8db4e7 | ||
|
cd4bd71e6d | ||
|
7ffa5b50e0 | ||
|
d973a80c4b | ||
|
eecc76efeb | ||
|
0589df6685 | ||
|
9bd3e5bfa6 | ||
|
e3e84fa7c9 | ||
|
5131a66552 | ||
|
7ac6c2ed5e | ||
|
d2d0658f8d | ||
|
95bcde4e33 | ||
|
508dfebcfc | ||
|
aa68e8ac5b | ||
|
d4e74fd345 | ||
|
778ae71848 | ||
|
857de4c175 | ||
|
923ab238fc | ||
|
bf5cde52bc | ||
|
5900b01df6 | ||
|
5d9ce5561d | ||
|
07d2b849f3 | ||
|
0ac84cb838 | ||
|
7158885ea6 | ||
|
239c4f8476 | ||
|
e519870dc6 | ||
|
5bccee0cf5 | ||
|
28bd30c303 | ||
|
9395027e4c | ||
|
965b1189c6 | ||
|
c834e4dfae | ||
|
a12b2ea313 | ||
|
299bc7f67d | ||
|
921f20333f | ||
|
094d280e1c | ||
|
7e1fbaa94f | ||
|
fe76ee16e5 | ||
|
8454b8fc92 | ||
|
9d7e1cc0a8 | ||
|
11e577e464 | ||
|
709f2d4fdd | ||
|
3110f76ce7 | ||
|
c9ba37eead | ||
|
1469ec3768 | ||
|
9b4b67c556 | ||
|
9f1422128b | ||
|
d0fd4b7c18 | ||
|
c0fc542359 | ||
|
f6a545597a | ||
|
9be834118c | ||
|
9d695886ad | ||
|
a2cb1837b2 | ||
|
48606912cb | ||
|
f8746b012a | ||
|
d1e5de2399 | ||
|
d3bf6670b7 | ||
|
48cfe281f7 | ||
|
e4ba5c9249 | ||
|
2449a5c00d | ||
|
cd66c810a8 | ||
|
e6903e8fd8 | ||
|
e9bd4d9756 | ||
|
c68a0b2f82 | ||
|
1e333892e1 | ||
|
ac9a41bc33 | ||
|
bdf4ec00dc | ||
|
fcb3bd663d | ||
|
725e77f229 | ||
|
41a8718f4a | ||
|
988c5c7488 | ||
|
0370b98684 | ||
|
df44356c66 | ||
|
8884336b96 | ||
|
438c8604a7 | ||
|
29ecbce4d6 | ||
|
cf24b943cd | ||
|
e12665e135 | ||
|
af4f3414ed | ||
|
4f56151caf | ||
|
544a5afb81 | ||
|
842b24ff39 | ||
|
57006e0aa0 | ||
|
63b2b9a2c1 | ||
|
f0e62cc9fe | ||
|
7d3ed2e7a3 | ||
|
299409e1aa | ||
|
a46327b6b2 | ||
|
1f15e2b96d | ||
|
c75a79c53d | ||
|
5f6751050a | ||
|
e4e098c9dc | ||
|
3e99892c8a | ||
|
213f58608e | ||
|
4c9dfdaa6d | ||
|
4d28203c38 | ||
|
3333cd244c | ||
|
49bf4872ca | ||
|
bb8c80beee | ||
|
b2bc9a976a | ||
|
3179852067 | ||
|
9745fd1382 | ||
|
c12f6b131d | ||
|
18b69e8b1b | ||
|
838402e09f | ||
|
bfb8c40489 | ||
|
2b1dd38980 | ||
|
135cce9201 | ||
|
38838c81b4 | ||
|
4809d82766 | ||
|
30d588723d | ||
|
78f0ef704c | ||
|
43bc97202e | ||
|
6b5df52c60 | ||
|
a0870050e0 | ||
|
5530fca33e | ||
|
0e14fe99b3 | ||
|
3a558a621f | ||
|
83d2c6ba84 | ||
|
d2d638fd26 | ||
|
42c295a63f | ||
|
26f6b5e2d1 | ||
|
5504bf3614 | ||
|
b34d4d2f4e | ||
|
c2028cc58a | ||
|
1b1addaab3 | ||
|
3603f849e7 | ||
|
333e9785a6 | ||
|
60b9a7f094 | ||
|
e8de33a4cb | ||
|
9ad1b27351 | ||
|
948f0b98d3 | ||
|
a70a1c8841 | ||
|
3b575f9770 | ||
|
6641cba33f | ||
|
8174de1572 | ||
|
a84bc710db | ||
|
fd8662bb9d | ||
|
296dff47b0 | ||
|
154e114113 | ||
|
c6157af222 | ||
|
fd450d84db | ||
|
18befc5ccb | ||
|
e0ea41b86c | ||
|
6c85ec3cc9 | ||
|
5dbd936710 | ||
|
48d4076e61 | ||
|
e0cff54c68 | ||
|
7880ef02c3 | ||
|
832b37ab8b | ||
|
ccfed44655 | ||
|
873c80e462 | ||
|
eef08302d5 | ||
|
f44e8cf54e | ||
|
c347347004 | ||
|
e87a76c12b | ||
|
cb9ddc7da2 | ||
|
9dbda8eb85 | ||
|
2cc43f828a | ||
|
55f3f077d7 | ||
|
3898c2e644 | ||
|
6b3485db2b | ||
|
66e2482a61 | ||
|
25debb2f5f | ||
|
5c8ad023f4 | ||
|
5e1aa09ebe | ||
|
582c25a633 | ||
|
7c1ddbceac | ||
|
b4edd6c41d | ||
|
1b7f95f231 | ||
|
11d28924a3 | ||
|
f36f8d2fe5 | ||
|
c23adb2379 | ||
|
09d6d1bd01 | ||
|
da485f2a15 | ||
|
ac0e70149a | ||
|
9fe264c951 | ||
|
211138e74e | ||
|
3008fb4339 | ||
|
ae5ece38d4 | ||
|
3ff8af26ad | ||
|
172c66ded8 | ||
|
ca59c55918 | ||
|
57c27aa7f7 | ||
|
1eb225180e | ||
|
ed62784a88 | ||
|
271fc5660a | ||
|
c5054d4621 | ||
|
91be786e42 | ||
|
979cd3cdc7 | ||
|
b7b99ef5c9 | ||
|
d1f08c2e4e | ||
|
f6969b3079 | ||
|
2a2eda5428 | ||
|
539c66e232 | ||
|
c6fc67702e | ||
|
dcc81c3850 | ||
|
2da2e1be8c | ||
|
d25dd0b6ba | ||
|
50fe2c82d6 | ||
|
6a7457d708 | ||
|
7915beb8b5 | ||
|
f9d9895f91 | ||
|
22c3273594 | ||
|
2502c70bba | ||
|
8806c66d22 | ||
|
ecbf49e256 | ||
|
8208b46d3c | ||
|
975cf5a63a | ||
|
d79799b329 | ||
|
d415a8ba15 | ||
|
0438da2276 | ||
|
3a66bd9555 | ||
|
c6136e7166 | ||
|
bf13fa2786 | ||
|
0682b30d4f | ||
|
e137ccd0f1 | ||
|
9992cfb7b8 | ||
|
1dca759741 | ||
|
f579283feb | ||
|
faa8e2a7be | ||
|
c424896496 | ||
|
80ba725772 | ||
|
86004beec1 | ||
|
898431025b | ||
|
92799af91d | ||
|
ce0574b41f | ||
|
3e63c01b51 | ||
|
d5b910b401 | ||
|
c4c8780740 | ||
|
2a822a1b7a | ||
|
c60fc69334 | ||
|
72b6131ce1 | ||
|
4f36ee4728 | ||
|
c5010a3a72 | ||
|
3927c8c47e | ||
|
85ea164e94 | ||
|
f9ba1dfccc | ||
|
638291a833 | ||
|
12000b2fa4 | ||
|
d74b8e6073 | ||
|
73b304df96 | ||
|
d711aa9f9a | ||
|
535676091f | ||
|
5bb0e2d7da | ||
|
99b8daa5e8 | ||
|
1b14e5bdb8 | ||
|
3f7b331c11 | ||
|
c671d8e54e | ||
|
88161e2f87 | ||
|
ccdb28cb85 | ||
|
cc821c6417 | ||
|
c860ce08d5 | ||
|
4cd61e7957 | ||
|
034badcee6 | ||
|
5fc429bfc0 | ||
|
4ad3910e3f | ||
|
f8bcdff3f7 | ||
|
3c5bf90f68 | ||
|
aa27d6dc7c | ||
|
a98573955a | ||
|
899ff30321 | ||
|
62598cb2c5 | ||
|
cf4a113f8b | ||
|
d58ff081b1 | ||
|
696fe7fc1c | ||
|
06ddef9edf | ||
|
d6d90363d9 | ||
|
a29e43cd7a | ||
|
c8a66de25c | ||
|
d79d51f30c | ||
|
9cd38be7b3 | ||
|
f64725ac63 | ||
|
0684b1c7fb | ||
|
c8c9fab965 | ||
|
903f129a04 | ||
|
542d0ff64b | ||
|
8081d815b7 | ||
|
46f8c7ecc0 | ||
|
527b84ad36 | ||
|
763ac3a578 | ||
|
658c2b0c8b | ||
|
a35007dcfd | ||
|
41042a0c07 | ||
|
3b0e0c0a1d | ||
|
b2cd279691 | ||
|
9cb1345125 | ||
|
696cfce612 | ||
|
b11af177d5 | ||
|
99380597fc | ||
|
78474e7614 | ||
|
4151a5656b | ||
|
51800cca55 | ||
|
2647a82ecb | ||
|
ef767c12ef | ||
|
a0ffab4085 | ||
|
86f81f55b5 | ||
|
53786ea7cb | ||
|
2dc55ce002 | ||
|
0379af934b | ||
|
f1a2aeee2f | ||
|
1337047a1f | ||
|
811473e911 | ||
|
3e1ca62004 | ||
|
c192221171 | ||
|
df9e22b490 | ||
|
46273b661d | ||
|
71f9d8d86d | ||
|
15904baa8c | ||
|
7da416a66c | ||
|
83d7b6c39d | ||
|
802e0a2211 | ||
|
49283919b3 | ||
|
c6c5b15c38 | ||
|
a31bd84c8b | ||
|
6adbd9ef64 | ||
|
1a2837c611 | ||
|
45189d4d4b | ||
|
a1985dc78b | ||
|
1ca59f4e48 | ||
|
0d34c8fe3b | ||
|
8f8dbbe6eb | ||
|
3df03677a0 | ||
|
4e782109a5 | ||
|
bb7132124c | ||
|
60f192001f | ||
|
0c7f3ab352 | ||
|
c7dc5d912f | ||
|
146cecff7a | ||
|
c071d9842b | ||
|
791bbc48e3 | ||
|
0cf01922d8 | ||
|
b6f8ba4f89 | ||
|
179d99a55e | ||
|
c7e074300b | ||
|
b6d399a6d1 | ||
|
997940de3c | ||
|
1a5b20b8c9 | ||
|
f4cac1b08c | ||
|
60d4ce2a61 | ||
|
283a333a23 | ||
|
44ae8e9d20 | ||
|
c875ec1d10 | ||
|
b446686545 | ||
|
ce508f1a84 | ||
|
22009cc9ef | ||
|
89336b3adc | ||
|
7c47d14ce2 | ||
|
7a29590bee | ||
|
fc9f8bfe6b | ||
|
7d8b067ed7 | ||
|
0bcd31660f | ||
|
817308b861 | ||
|
176948dd66 | ||
|
d7268c3eb7 | ||
|
13e54c5d58 | ||
|
b6282976ba | ||
|
ced701f1f0 | ||
|
7b7f8f7fcf | ||
|
013c917dc1 | ||
|
e9647c6876 | ||
|
fd84d566fd | ||
|
2693d4364c | ||
|
08f77c25c1 | ||
|
0102b303ab | ||
|
fcf430ce05 | ||
|
289595f5da | ||
|
ddfccf5a33 | ||
|
fc91ff5911 | ||
|
d522984df7 | ||
|
e86c28f60c | ||
|
405b5d8bab | ||
|
c7561e6b36 | ||
|
513d169e69 | ||
|
1e9dd7c61d | ||
|
f1c9529de8 | ||
|
d84154bc5b | ||
|
3e114bdd3d | ||
|
0ff1f99f21 | ||
|
134b1978e4 | ||
|
85f68c8cfb | ||
|
0e8ae600e0 | ||
|
0356b60014 | ||
|
ce9b6823d5 | ||
|
cdce2eee6f | ||
|
b3ffa72be2 | ||
|
d6eb489414 | ||
|
8623147181 | ||
|
4150d48420 | ||
|
414504b5f8 | ||
|
acde0549c3 | ||
|
a7c8a2858b | ||
|
0790adb276 | ||
|
b2ad40acbe | ||
|
91ca5fe16a | ||
|
c45bae5e34 | ||
|
164e42767d | ||
|
c22119fa99 | ||
|
609b1c9c1e | ||
|
74134a7309 | ||
|
6b4d90a77d | ||
|
07657c932d | ||
|
ab59958035 | ||
|
a2fdd6eb97 | ||
|
390c065759 | ||
|
4d0641c2ca | ||
|
8018227917 | ||
|
ee0262b68a | ||
|
e91ec612d3 | ||
|
05591ab40d | ||
|
e0986d7382 | ||
|
b546243cac | ||
|
750ea6d94a | ||
|
6996d0c9e0 | ||
|
0fe594cf4b | ||
|
f9d2d6a2e2 | ||
|
24d391311e | ||
|
d474817bb5 | ||
|
c106a1e2a0 | ||
|
56b7d508c1 | ||
|
d9aa60bbf7 | ||
|
1cf88202aa | ||
|
78c26a94ea | ||
|
28170c023f | ||
|
638cdc068c | ||
|
943a978f48 | ||
|
ed035739f1 | ||
|
662803be0b | ||
|
76c0804e82 | ||
|
8303bd9cfb | ||
|
f3d46dda94 | ||
|
ca6427b1ab | ||
|
9f51faab6d | ||
|
f4dc00514a | ||
|
a23572655f | ||
|
59d3654100 | ||
|
5164e35080 | ||
|
861b0f6063 | ||
|
88b2627a46 | ||
|
f7e8527e97 | ||
|
96a0ec1734 | ||
|
887c3fd2f6 | ||
|
487da32ab3 | ||
|
c41fe74b94 | ||
|
99fcf191bd | ||
|
2abd48221b | ||
|
ccee5a008b | ||
|
96662679df | ||
|
df62b5481c | ||
|
7079814e04 | ||
|
68971062cf | ||
|
2dd105df5e | ||
|
d11bc6a95d | ||
|
5f4223ab38 | ||
|
459c3281f5 | ||
|
3799d008cb | ||
|
c07ef8d584 | ||
|
776898e915 | ||
|
17e020cb42 | ||
|
28bfcef2cc | ||
|
f60d589621 | ||
|
3df91e4457 | ||
|
1acdd57c34 | ||
|
551865c289 | ||
|
ae72dd704f | ||
|
c10094e6bd | ||
|
3da81a84c3 | ||
|
ad8ecd79e4 | ||
|
8aff7a2785 | ||
|
62008dd4e3 | ||
|
ff2ddd35b0 | ||
|
68120fdc58 | ||
|
0501c5d892 | ||
|
932342852f | ||
|
1065eb6dd8 | ||
|
85b9ce8111 | ||
|
89d4003362 | ||
|
0049ba0ee1 | ||
|
7853d51413 | ||
|
d6ecbf1e10 | ||
|
827452262f | ||
|
d83dc5c09c | ||
|
51101a766f | ||
|
5248a439e3 | ||
|
38fc0ace6c | ||
|
33973be567 | ||
|
9119711401 | ||
|
f23bb58774 | ||
|
b9f27ed5fb | ||
|
2a0982936a | ||
|
5df65f07d2 | ||
|
f24ae02f5c | ||
|
6079da3770 | ||
|
03d5c7ea7b | ||
|
1e7a57e8a4 | ||
|
41dc8863fa | ||
|
967bfb447e | ||
|
3a6e1f8ed0 | ||
|
2ee2208459 | ||
|
cfc70b7049 | ||
|
e83a8486e6 | ||
|
b88412c72a | ||
|
b57e79ded9 | ||
|
9743b2297b | ||
|
fddf896482 | ||
|
77ecf9d35c | ||
|
9b510a7c0a | ||
|
336de88b2e | ||
|
09a24a0989 | ||
|
f7b62883a2 | ||
|
2ac1045bb5 | ||
|
300ae70e35 | ||
|
e45d4a622d | ||
|
65a7fa3e46 | ||
|
4ceaeab1c7 | ||
|
4d9635bef2 | ||
|
f51e3c4212 | ||
|
8961fe2ca3 | ||
|
94f71738f8 | ||
|
945ce0d693 | ||
|
48dbc2294f | ||
|
4602ce5172 | ||
|
cdb4a1839e | ||
|
f4f753604d | ||
|
38d2ba525f | ||
|
df3dc42b4f | ||
|
aee00ffdc0 | ||
|
fe3a5da0bf | ||
|
63c58a8849 | ||
|
8b01153b0f | ||
|
99a2b3dcc3 | ||
|
1709e8d79e | ||
|
77934801dc | ||
|
f4e6ac9316 | ||
|
0d1adb4708 | ||
|
6c40c5f848 | ||
|
4e388d65b1 | ||
|
3a02337974 | ||
|
e431c52895 | ||
|
3cc3575339 | ||
|
baa0b7732a | ||
|
b0b7640ff2 | ||
|
6330896863 | ||
|
57ac096cca | ||
|
e2160861df | ||
|
4678b1e3c3 | ||
|
20e21df351 | ||
|
afc2630b81 | ||
|
9b2e0916b7 | ||
|
5558351f90 | ||
|
024c9f5045 | ||
|
34f7efd862 | ||
|
01d9b66de0 | ||
|
6185abf85f | ||
|
428cdde36b | ||
|
071cd67ca0 | ||
|
847dff1fab | ||
|
ad8e5b5604 | ||
|
a1d4e93502 | ||
|
e91e17813f | ||
|
cfb0236719 | ||
|
e0743fb930 | ||
|
1a86db1428 | ||
|
621262b949 | ||
|
d500816715 | ||
|
ce450908d2 | ||
|
e4069d534f | ||
|
e71b2e0298 | ||
|
c826d59bf8 | ||
|
e8140e293e | ||
|
6fa9a2c6b6 | ||
|
7291363c86 | ||
|
1543a1cc5c | ||
|
1cc05e9a39 | ||
|
937824b393 | ||
|
622f26023e | ||
|
e449446b2c | ||
|
9d20d9ba43 | ||
|
cdfd909fa8 | ||
|
143af85d15 | ||
|
0ba6bcff27 | ||
|
6066006f39 | ||
|
6a24dfb9ca | ||
|
547939809f | ||
|
798efe06a6 | ||
|
6b6fbfbcd5 | ||
|
4050829985 | ||
|
6d3fecf57c | ||
|
cf497d2457 | ||
|
2443394329 | ||
|
c53cd4366c | ||
|
2c7a1b5e26 | ||
|
e9e93a36ad | ||
|
8863c679d7 | ||
|
b85a8df73c | ||
|
e94ab04cb9 | ||
|
bb464e92bf | ||
|
cad6430de7 | ||
|
524325c343 | ||
|
2d5eb62cbf | ||
|
247d77ea77 | ||
|
d7e624d5b6 | ||
|
1b9a24ead5 | ||
|
7477026fc3 | ||
|
82907ebae1 | ||
|
d691070e00 | ||
|
8d8905da92 | ||
|
09c088074b | ||
|
9adf3576e1 | ||
|
5401a19a9b | ||
|
144ec51a31 | ||
|
419b7d27d0 | ||
|
f62bf09799 | ||
|
83a535594f | ||
|
4ca590476b | ||
|
d7e30242ab | ||
|
77079ba251 | ||
|
258b3a2583 | ||
|
38374747ca | ||
|
902076d542 | ||
|
5115fde6fd | ||
|
111cc69287 | ||
|
732c3081fe | ||
|
c74efc6a24 | ||
|
35e1325771 | ||
|
da00a09045 | ||
|
f6ad1dcb97 | ||
|
483ca487e4 | ||
|
fb09fe35f6 | ||
|
b8375097ab | ||
|
c18a653c7d | ||
|
04a89e7ad7 | ||
|
c1f1873570 | ||
|
f184c4495e | ||
|
588ddfb775 | ||
|
cc537062fa | ||
|
b1b5ae370c | ||
|
9db6146a82 | ||
|
4edf9e812e | ||
|
82a3815026 | ||
|
7bc2690d96 | ||
|
37c9db98e8 | ||
|
cb3a1b3f1c | ||
|
8a40f7440a | ||
|
4dc7462a29 | ||
|
467925b169 | ||
|
989a6df0d2 | ||
|
20e051dfc5 | ||
|
669f959ab5 | ||
|
8263d55665 | ||
|
0c192923cb | ||
|
9235619811 | ||
|
1362d3bb88 | ||
|
abbc226e85 | ||
|
8501b42ec0 | ||
|
5c250e71c5 | ||
|
dac44a0828 | ||
|
dd6bce6f96 | ||
|
730224c75a | ||
|
c61ccc5de2 | ||
|
99c7422f89 | ||
|
59c4f74cf5 | ||
|
6eb95a5c4f | ||
|
57eb471686 | ||
|
accca39658 | ||
|
f78b30c3d5 | ||
|
460d63a2df | ||
|
f63e51063f | ||
|
ce2eeb83af | ||
|
e9df08e33e | ||
|
40d9d25929 | ||
|
0f6cffb6f2 | ||
|
838bbbe54e | ||
|
ef695feb92 | ||
|
c9afd6f062 | ||
|
d6786d8ce2 | ||
|
e0fa896e4b | ||
|
5e5d6c6b41 | ||
|
7462accac6 | ||
|
71615887da | ||
|
b82e4738b2 | ||
|
05cd2af1b4 | ||
|
2d7f0d2dcb | ||
|
0be5370132 | ||
|
b28bf9598f | ||
|
81610bb6f3 | ||
|
7054a9e73c | ||
|
7dbec481aa | ||
|
c774c95a74 | ||
|
d5650c2bb0 | ||
|
9b61c9fc42 | ||
|
8b5fa1e368 | ||
|
2ce0c38bf6 | ||
|
f134db4d30 | ||
|
9a9d156edf | ||
|
cda35e4a97 | ||
|
422d28235d | ||
|
f9b6908a17 | ||
|
7a35234521 | ||
|
dea7624148 | ||
|
76ad88d16b | ||
|
d783f03021 | ||
|
adcdb155be | ||
|
3077f6eb57 | ||
|
b4e651e22b | ||
|
33f2a28e8e | ||
|
e05945319e | ||
|
154342968a | ||
|
8077503dd6 | ||
|
eb5889c88e | ||
|
b66161f0d0 | ||
|
96c31d8551 | ||
|
ad826920d1 | ||
|
4d7dc2f471 | ||
|
79d0a390e7 | ||
|
6b833e6131 | ||
|
70e5f55d17 | ||
|
3b04b290f5 | ||
|
242067d20e | ||
|
5f1d344972 | ||
|
76cf8d8608 | ||
|
003918de1d | ||
|
7092d7e8c0 | ||
|
7940335de3 | ||
|
5f3d13ee08 | ||
|
d6060bc41c | ||
|
f3d2685afb | ||
|
2977a87b2a | ||
|
d871b7eebf | ||
|
de319721e2 | ||
|
4906f08d3b | ||
|
d925cbf515 | ||
|
513423e7d7 | ||
|
227124f6c0 | ||
|
fc507293d7 | ||
|
3cd543bde3 | ||
|
30d5063bea | ||
|
ca72490cf6 | ||
|
288f3f4e21 | ||
|
64f9842e28 | ||
|
4f0bcad508 | ||
|
1ed959a90c | ||
|
7a767ac8ac | ||
|
809d064db9 | ||
|
6cc77f9fd0 | ||
|
1eb955e97b | ||
|
4238e9b343 | ||
|
40093ab1c8 | ||
|
2fc710a49b | ||
|
c98dd54a8a | ||
|
883a8d44f5 | ||
|
1768cc0380 | ||
|
29e99fe3a2 | ||
|
d54a32910a | ||
|
a52d2c31a5 | ||
|
039b1be06d | ||
|
10e89e74ca | ||
|
9ba60356ae | ||
|
48548ec3c1 | ||
|
79506c72b3 | ||
|
a058818398 | ||
|
72a6fc1e85 | ||
|
f4658681f4 | ||
|
cd32176931 | ||
|
a5906ce016 | ||
|
7561975bb1 | ||
|
e851df596e | ||
|
89bd7f0cfc | ||
|
d0c67241bf | ||
|
2f372c6c8e | ||
|
a4a1ec30ee | ||
|
881ec27448 | ||
|
4c2817a0d8 | ||
|
48c284664a | ||
|
53644988df | ||
|
44fd79880d | ||
|
d92226754a | ||
|
fca6b99f96 | ||
|
1b7ba91a40 | ||
|
2973a5c1f5 | ||
|
e41f2d438e | ||
|
56515bc554 | ||
|
d9e3613f55 | ||
|
4ddff2c106 | ||
|
9c646c4bcc | ||
|
f8f9b8871c | ||
|
9ab9648bd4 | ||
|
458d8475ac | ||
|
a5f3573a7c | ||
|
70ff1351fa | ||
|
7a17acd64c | ||
|
2eb04e4d5e | ||
|
cfc79c6748 | ||
|
7540d0df31 | ||
|
4aec10ce36 | ||
|
09c95597d3 | ||
|
14dfcae598 | ||
|
3936f87906 | ||
|
d8e2cb2078 | ||
|
3b57e87f14 | ||
|
a319f22948 | ||
|
0bbb7dbe9f | ||
|
9c46fc2d06 | ||
|
b7981c5b9e | ||
|
cff7fbfabd | ||
|
8ba8aede9f | ||
|
3f93ecb065 | ||
|
df670045c4 | ||
|
b685ccbf52 | ||
|
91ef8616f5 | ||
|
dab2ee9744 | ||
|
cd5a9492bb | ||
|
220de770ba | ||
|
eb672e4592 | ||
|
86482f8cbd | ||
|
4f5cce506f | ||
|
8b260ca5fc | ||
|
b2b4713dde | ||
|
0ae81f7822 | ||
|
db867f7f46 | ||
|
9251aa6e51 | ||
|
3174469574 | ||
|
feff35537a | ||
|
41ed13fcb7 | ||
|
a83afb43ca | ||
|
d1008a74d0 | ||
|
00eb85df1f | ||
|
391c171f66 | ||
|
437f102b7b | ||
|
0dabaf88ce | ||
|
879bd4d47c | ||
|
a8a8ea767b | ||
|
acee9219bd | ||
|
ca3598ffc6 | ||
|
9285bbc4a5 | ||
|
cbf5a7817e | ||
|
a8de8f621d | ||
|
82d68fecd2 | ||
|
782ba6a356 | ||
|
7575d05d8d | ||
|
bcecb471e1 | ||
|
2ba5afef71 | ||
|
a6b5a63eaa | ||
|
b0dfeb9605 | ||
|
a2a5e1263c | ||
|
f74bc9ba30 | ||
|
557da4f47c | ||
|
46e2eac702 | ||
|
84c205fdae | ||
|
5daedb85d4 | ||
|
8b175beb1b | ||
|
52cf7ec604 | ||
|
573113a6a1 | ||
|
d1f81db733 | ||
|
b4d4e72808 | ||
|
a9b06019a6 | ||
|
881dde509f | ||
|
316fcdd290 | ||
|
119cc9e446 | ||
|
1f987d9bbd | ||
|
748f7a2256 | ||
|
419420b7a7 | ||
|
20a6d7fefd | ||
|
391ff5a88f | ||
|
8aa86c6286 | ||
|
daf9a4d040 | ||
|
672a712f69 | ||
|
523fe5bcf6 | ||
|
f7d37df8e0 | ||
|
4d5026c8b1 | ||
|
1c8e478714 | ||
|
6489bbc767 | ||
|
68de064fd1 | ||
|
40c6d87926 | ||
|
bf4e1f1877 | ||
|
33b399479e | ||
|
3fdc1a3d40 | ||
|
392c1959fe | ||
|
80849566c1 | ||
|
210b259138 | ||
|
b7e02b49ba | ||
|
906772bd98 | ||
|
4b6264f967 | ||
|
e10bd68dfb | ||
|
826635a76f | ||
|
192f41b506 | ||
|
927f5bd9a9 | ||
|
78c98f53e7 | ||
|
99e43d6d0c | ||
|
6782c41416 | ||
|
7d088f7003 | ||
|
ef9cf95b51 | ||
|
69a0fca159 | ||
|
47bda55136 | ||
|
d5cc8dcdb3 | ||
|
6c6f2b706c | ||
|
5642fea119 | ||
|
d79331a912 | ||
|
afd051ac4c | ||
|
dfeee97cf7 | ||
|
63c7bc5162 | ||
|
a44f943dd4 | ||
|
14e721df87 | ||
|
6d82f92c75 | ||
|
db735f92a7 | ||
|
53943fa5c8 | ||
|
88e37edbb5 | ||
|
253b1d5ac9 | ||
|
ed9f677515 | ||
|
89b16bf342 | ||
|
4cf1b315dc | ||
|
e63e3fd740 | ||
|
14c9fd9e70 | ||
|
b8aef443d7 | ||
|
e19f889024 | ||
|
3a0c585fc5 | ||
|
a7730ddf7e | ||
|
63c438cc2c | ||
|
2c12c3c66e | ||
|
03c223831c | ||
|
e1c60f9f8c | ||
|
8b9487b175 | ||
|
4160ec646a | ||
|
91600a25c3 | ||
|
12d6854ff1 | ||
|
130407c0d4 | ||
|
2e42fcb898 | ||
|
e6e04990f4 | ||
|
999d96d8c2 | ||
|
713546522b | ||
|
1d6d917e6d | ||
|
89f9536724 | ||
|
4620d3d285 | ||
|
d7dad23dcc | ||
|
49f5a7d9ea | ||
|
337d5b634f | ||
|
3409e76540 | ||
|
f3cc799f56 | ||
|
bfde00968c | ||
|
57ce8975fd | ||
|
a85998392b | ||
|
c0aaeab3a7 | ||
|
bd89d4e210 | ||
|
16d1d663c4 | ||
|
caf24ca26c | ||
|
aaa7cbe01a | ||
|
dd22edc551 | ||
|
5f24107e35 | ||
|
a1f355bf43 | ||
|
13269cf491 | ||
|
8ffb412644 | ||
|
223cfbbd73 | ||
|
a772d88ce4 | ||
|
de9340b91d | ||
|
f1468bcc30 | ||
|
b8ddfec02c | ||
|
746b97f95b | ||
|
8b45a9a259 | ||
|
f84165c885 | ||
|
aa83bf4300 | ||
|
04428a4664 | ||
|
9306634502 | ||
|
feec9a8881 | ||
|
fa65663a22 | ||
|
806bba0824 | ||
|
5582215b41 | ||
|
1faa29ceaa | ||
|
eb066859fa | ||
|
8c02f5eb70 | ||
|
7158aec4d0 | ||
|
a116728037 | ||
|
cb0c6c6fe2 | ||
|
642de97554 | ||
|
999c1077da | ||
|
0319a11a4e | ||
|
0694f0d611 | ||
|
152eb5213c | ||
|
040943d8cb | ||
|
35da75095e | ||
|
62cb81a353 | ||
|
79e715b639 | ||
|
63ce0f2de6 | ||
|
2f965d5b14 | ||
|
deaee91575 | ||
|
3cff77a4bc | ||
|
6f6a7da004 | ||
|
5f1f7006b7 | ||
|
3d21b8c831 | ||
|
9c93c8a20c | ||
|
b736071d50 | ||
|
5aaf93fde5 | ||
|
ff31f2feef | ||
|
3860a0b6f7 | ||
|
4633ae2c20 | ||
|
0dde27ca3f | ||
|
2d7c37c6d9 | ||
|
4cbc041aff | ||
|
40a712f126 | ||
|
4cecc44208 | ||
|
a531be755c | ||
|
36091de552 | ||
|
894d113668 | ||
|
45816f20b4 | ||
|
c6dde26202 | ||
|
56236ce97a | ||
|
e6f04628a7 | ||
|
072df2cc3b | ||
|
265e3541ea | ||
|
b6a98f0d3b | ||
|
b1d5cfaba4 | ||
|
ce68cbd067 | ||
|
71535e7fc1 | ||
|
83887c9913 | ||
|
2fa41bb61b | ||
|
ad5f92665f | ||
|
0e1f91ef6b | ||
|
dfb356e597 | ||
|
51dbce5c56 | ||
|
c0be69597b | ||
|
f82cc595b5 | ||
|
5fe1a405a2 | ||
|
ae69db823b | ||
|
a31f0d3029 | ||
|
0dcf1d84c1 | ||
|
e95214f4f7 | ||
|
b5077a4afe | ||
|
e76187526b | ||
|
9c7a930759 | ||
|
fc2b6823b3 | ||
|
3a244fdf05 | ||
|
6326ee53de | ||
|
ebcb9febce | ||
|
39fbec1362 | ||
|
a6c32f1abb | ||
|
f15c147c02 | ||
|
2415a1690f | ||
|
727c703678 | ||
|
a6d7719535 | ||
|
6bc0255e75 | ||
|
2cad4e53c4 | ||
|
0842ebfaed | ||
|
e0ee32107d | ||
|
62e57312fe | ||
|
db92b190f2 | ||
|
b0027fe531 | ||
|
f31a9cbcdc | ||
|
2b36c1ffa0 | ||
|
482188a413 | ||
|
a68b6b9591 | ||
|
9d06681a84 | ||
|
62287c0ff7 | ||
|
6bc157751a | ||
|
2fca13d553 | ||
|
114c45eb1d | ||
|
03188fd5b6 | ||
|
b6cbd97b1d | ||
|
96e5c5767a | ||
|
d75a52ffe9 | ||
|
3c650c2ec7 | ||
|
8ebaa175f2 | ||
|
4e44f19ff4 | ||
|
4bd21d3647 | ||
|
878e786eae | ||
|
76fa3dfa76 | ||
|
cbd376e698 | ||
|
a29a1895b3 | ||
|
faf87dc7ca | ||
|
15166d7fef | ||
|
184119de1a | ||
|
1860aa7afb | ||
|
78ca24d5f8 | ||
|
6217cc8aec | ||
|
1635965f89 | ||
|
577f39b7ef | ||
|
57ee72065d | ||
|
7c662e4a74 | ||
|
4beb1c02a4 | ||
|
0084ea446d | ||
|
f29daf0293 | ||
|
dc4fe3a722 | ||
|
58ba6c0543 | ||
|
08c0a07040 | ||
|
2ca67d1096 | ||
|
ccbfbe286b | ||
|
c7dfbd9472 | ||
|
88107e4b0b | ||
|
605dfa6e68 | ||
|
59ed6a21d1 | ||
|
1fe97e781c | ||
|
6f71ae72a3 | ||
|
4254dbfed6 | ||
|
89d21f54cb | ||
|
88fca08bac | ||
|
557e47d57b | ||
|
8f24a763bf | ||
|
166afeca5e | ||
|
610ba6de6c | ||
|
e418d63b2e | ||
|
0476187502 | ||
|
5e96e2fd58 | ||
|
0397799744 | ||
|
0a8cbebda0 | ||
|
a6e5596b90 | ||
|
2dcbc3e6ee | ||
|
365b15a9df | ||
|
dede16d5d1 | ||
|
c668caf642 | ||
|
5038a6ab2f | ||
|
a19a8d834a | ||
|
cb234039dd | ||
|
3afacf8158 | ||
|
b139aad82d | ||
|
cd69669fdc | ||
|
f9e81398a3 | ||
|
aeb43b96bb | ||
|
8207ec33eb | ||
|
2e3735c91e | ||
|
76f6c99dac | ||
|
0933ff9354 | ||
|
df101f8595 | ||
|
383a723e76 | ||
|
248a9ae319 | ||
|
ded349488f | ||
|
336e11d762 | ||
|
e572491e8a | ||
|
57a189ed69 | ||
|
efb99fc333 | ||
|
f14510f68d | ||
|
beb2dcc23b | ||
|
439a69c88f | ||
|
bce7d5e0a5 | ||
|
35da943b22 | ||
|
2952cb000a | ||
|
58eebce2ae | ||
|
8bbc187dd0 | ||
|
5fe8b14832 | ||
|
cbfb2fce1f | ||
|
8b71333466 | ||
|
e5cf219f73 | ||
|
a43704c858 | ||
|
d9827f4c5f | ||
|
5f739b7957 | ||
|
e721119e45 | ||
|
52edb23d8a | ||
|
9ad21f6529 | ||
|
2a32564a12 | ||
|
8130a4a63b | ||
|
58fa87180c | ||
|
4676566e72 | ||
|
9dc84afd96 | ||
|
44de5b2913 | ||
|
806baa2f36 | ||
|
10d7913f1f | ||
|
952033813e | ||
|
f62a427f36 | ||
|
37e8487213 | ||
|
51d6209b27 | ||
|
f2c6394d7c | ||
|
6766244e86 | ||
|
8584abe2ec | ||
|
df3d233e35 | ||
|
0fb10ad1f3 | ||
|
b832a54120 | ||
|
176131f342 | ||
|
eef561a61c | ||
|
8106d19c79 | ||
|
ae3a869a61 | ||
|
8ed192879f | ||
|
40d1a34567 | ||
|
908a5f6bd3 | ||
|
bd88bea872 | ||
|
f1bd035824 | ||
|
a6def314cd | ||
|
3e3be30384 | ||
|
1785cc1238 | ||
|
8e36c80359 | ||
|
d2e9ef76bf | ||
|
01a6efa56d | ||
|
72af78f64a | ||
|
33676a1afc | ||
|
1752cc6123 | ||
|
03ce2af1ea | ||
|
f074ab945f | ||
|
f14aaa8c44 | ||
|
a54df35079 | ||
|
4eceae60ff | ||
|
8511fc55d0 | ||
|
a12c21ed9e | ||
|
d5581230e1 | ||
|
a1f834aadd | ||
|
ee2986c627 | ||
|
aa3b041853 | ||
|
b946837899 | ||
|
9c3e81f83d | ||
|
319a9dd0df | ||
|
337946e800 | ||
|
0fa56d4a3d | ||
|
00ee2103c0 | ||
|
7dcd7b45c7 | ||
|
fa2fd3d704 | ||
|
6715e2379f | ||
|
ebc4f46592 | ||
|
875ed8ea71 | ||
|
06474fbf2d | ||
|
171b688bae | ||
|
0c7f228f4c | ||
|
7bc7e1d53a | ||
|
4a77aa8019 | ||
|
1d95facfb2 | ||
|
a6a6df9e43 | ||
|
dc18e75541 | ||
|
d7655db15e | ||
|
02e107748f | ||
|
3d69b030b2 | ||
|
da9084035d | ||
|
0f04250953 | ||
|
ed0a7cd9a9 | ||
|
e7636a006e | ||
|
58eeb03ce3 | ||
|
1c31be5a70 | ||
|
5fbf0e9705 | ||
|
db91125e93 | ||
|
0900fe7e3b | ||
|
2ccc9a07da | ||
|
2fe3f4a448 | ||
|
19b5d7b39e | ||
|
56f3073ace | ||
|
c87901cbff | ||
|
d973e5c91a | ||
|
a087d1d1dc | ||
|
1b48ea8284 | ||
|
232bcae99c | ||
|
3707fdd825 | ||
|
f328e4e85b | ||
|
89b679ac3c | ||
|
e63309d6d2 | ||
|
5082c925bc | ||
|
adbbeb89bc | ||
|
e2bf83565c | ||
|
ae30ade221 | ||
|
2aee0c43fd | ||
|
fa7e0918a8 | ||
|
bb28f46ab5 | ||
|
62abce9a10 | ||
|
5780912983 | ||
|
23e0694bbc | ||
|
bde382a9fd | ||
|
5543ff94d7 | ||
|
c7d90c296e | ||
|
75d766cba6 | ||
|
405ff781f3 | ||
|
1cb7d9604e | ||
|
5b3da30ab2 | ||
|
67494320fe | ||
|
3cb0b7ecfd | ||
|
3b89c0f153 | ||
|
90642965d0 | ||
|
63947ec208 | ||
|
3c2f4ad972 | ||
|
7b5496fd09 | ||
|
b08c1e4412 | ||
|
efd0a788d2 | ||
|
cfeb9c5a0f | ||
|
5a94668399 | ||
|
14ff2cf21d | ||
|
76b00e3997 | ||
|
d69788bec1 | ||
|
e34ed52672 | ||
|
f0fc6bf7b5 | ||
|
c2f3c34ba0 | ||
|
b77101c4a2 | ||
|
61f4436834 | ||
|
8a0aceae33 | ||
|
19382ff5a1 | ||
|
34c11f74d2 | ||
|
785efe8937 | ||
|
c99b9c7ba7 | ||
|
3ec6240689 | ||
|
21185ad935 | ||
|
c586b665e7 | ||
|
9a8249766e | ||
|
6421235adb | ||
|
8f88a7eaa8 | ||
|
9efe27dbd6 | ||
|
255bd7e7d0 | ||
|
1c85ed7a38 | ||
|
21070e672e | ||
|
180fbead1e | ||
|
84da3fc00a | ||
|
3683a218b2 | ||
|
a276dc7fe1 | ||
|
61fa2399fa | ||
|
62805f110f | ||
|
397346010f | ||
|
7734bd6603 | ||
|
c9891ca437 | ||
|
a2301c355a | ||
|
036038276e | ||
|
402a85eaa3 | ||
|
dc272d6978 | ||
|
367c68306f | ||
|
4d9a6291a3 | ||
|
d80cd715b1 | ||
|
398f3355ef | ||
|
3c104c39aa | ||
|
b69c78f331 | ||
|
4a8e1e5cd3 | ||
|
91a38c6567 | ||
|
d11550d002 | ||
|
adaffbac36 | ||
|
1b79332755 | ||
|
ebe6a07093 | ||
|
b4253ca9ac | ||
|
05da1e5f38 | ||
|
ab1a4a49e1 | ||
|
1a91a45644 | ||
|
4c55f2e6de | ||
|
8c62debfbd | ||
|
c12cadfbb4 | ||
|
ce9bc642ad | ||
|
94af806256 | ||
|
b1cf7f259b | ||
|
09dba4289d | ||
|
f1832f8a2b | ||
|
41bb812bd6 | ||
|
9b208340f1 | ||
|
d02e57fd4c | ||
|
88dedea71a | ||
|
2beca6fd65 | ||
|
69f4f6faf7 | ||
|
ab78243dab | ||
|
3be04cdcaf | ||
|
ca36847b65 | ||
|
ae7965df67 | ||
|
5c48c5a0c2 | ||
|
a266f291b2 | ||
|
ab4a464d00 | ||
|
cce825fd76 | ||
|
75329f39cc | ||
|
c255cf515d | ||
|
d67ef8171e | ||
|
29e78f8541 | ||
|
e309a63fdb | ||
|
7babb39432 | ||
|
e343b2405d | ||
|
72192f90cf | ||
|
99ba0ae781 | ||
|
682a1ec9b4 | ||
|
b366d2e165 | ||
|
0645aec1ac | ||
|
9c88ff8381 | ||
|
4235bdd677 | ||
|
082caf2c06 | ||
|
a312d93413 | ||
|
eca6ea5b75 | ||
|
95f4676411 | ||
|
6960b1905f | ||
|
cba43b8507 | ||
|
2c4e3efd00 | ||
|
41a51ece86 | ||
|
e4e05c7fc1 | ||
|
e708f0f1e1 | ||
|
a000d14436 | ||
|
2c77e4b4f9 | ||
|
282213b679 | ||
|
046f77647a | ||
|
d681113ed1 | ||
|
b95e24ffef | ||
|
30dc23daa2 | ||
|
7db8a19f75 | ||
|
faee336de6 | ||
|
892fc66d9d | ||
|
a919f85afe | ||
|
45df472b18 | ||
|
d0c428fd69 | ||
|
2b7b906c9a | ||
|
c15bd1dd5c | ||
|
f407e0f088 | ||
|
47f85eacdf | ||
|
2457bf46bf | ||
|
8cdf2c7f87 | ||
|
64af9aad4e | ||
|
cb76bb8bdb | ||
|
b0156d33e4 | ||
|
88038ecefd | ||
|
f792298567 | ||
|
20ee86939d | ||
|
102d832779 | ||
|
349c60fbd4 | ||
|
2787a86fb0 | ||
|
f985f1fd70 | ||
|
308da261b9 | ||
|
b913d4b94f | ||
|
90f5330079 | ||
|
32b6ddb991 | ||
|
fc63a1def4 | ||
|
2b64ac6c2c | ||
|
4d1dbf9352 | ||
|
86994f814d | ||
|
03faf06737 | ||
|
671f58613d | ||
|
d84e8c5c37 | ||
|
90c771ebda | ||
|
62d3ed6033 | ||
|
34054c5bad | ||
|
afae4c9634 | ||
|
1a0f27fdcc | ||
|
5b4c595ef7 | ||
|
808e742dc7 | ||
|
ff5df69c91 | ||
|
11e535a719 | ||
|
c99eb9f6c9 | ||
|
b927d164f8 | ||
|
0a49491baf | ||
|
46194a2a78 | ||
|
66d8a4ef15 | ||
|
cb3885c9ca | ||
|
13200a5981 | ||
|
640753df47 | ||
|
39b5e5485c | ||
|
967c819f1e | ||
|
0a9c6c3374 | ||
|
33dc16110b | ||
|
0b91eba7ad | ||
|
1b4f8d6435 | ||
|
fae530a8a3 | ||
|
24cf8f1c69 | ||
|
e96991cc34 | ||
|
e9b81da5de | ||
|
af31c9e115 | ||
|
b86f6ccc49 | ||
|
05fb8fc634 | ||
|
466dabc18b | ||
|
86f47381a4 | ||
|
80f653ef15 | ||
|
ec79e62c32 | ||
|
8cabcb3925 | ||
|
febd9121ee | ||
|
1e94ad9d0b | ||
|
32ec346296 | ||
|
788a42b209 | ||
|
dc9fcd5605 | ||
|
1fff35c73e | ||
|
e232b8bc71 | ||
|
83f941591d | ||
|
f2b7111ea8 | ||
|
0995a09fa0 | ||
|
ba87f193ba | ||
|
47d92236cd | ||
|
e25e0cd2f7 | ||
|
44a5abbdcb | ||
|
da703b4422 | ||
|
a5cff5ca86 | ||
|
dbe309f319 | ||
|
d86e40c6dc | ||
|
f8bd32ea49 | ||
|
8af1cb440b | ||
|
93eda97088 | ||
|
654cf82ed6 | ||
|
5112a0869f | ||
|
82f5103639 | ||
|
9bd268c72f | ||
|
a2a82d73cc | ||
|
93c1e78628 | ||
|
61a0de5b89 | ||
|
4c24de5bae | ||
|
0ff31e7b70 | ||
|
65257b6a6f | ||
|
b24b1c3c0a | ||
|
cfb9f27edb | ||
|
82732efb45 | ||
|
7ee0d181bd | ||
|
321fc81bf4 | ||
|
2bc3d989ea | ||
|
1f02c83a77 | ||
|
58cc2a3282 | ||
|
d35752b143 | ||
|
e629963e44 | ||
|
09d62ee96d | ||
|
eda1807c05 | ||
|
0a08854be2 | ||
|
217a58874d | ||
|
af8505dcee | ||
|
99b073cbea | ||
|
a1183f8b91 | ||
|
c6db879414 | ||
|
9b9cfdafcc | ||
|
c038f50dc9 | ||
|
94a462175a | ||
|
fe7d91422c | ||
|
6a6190f585 | ||
|
55682f322e | ||
|
f9fcea2234 | ||
|
1919e1ad96 | ||
|
be3a3f0cf0 | ||
|
6c5dd577d8 | ||
|
3fe7edda4b | ||
|
2fc7a4719c | ||
|
2c5cef4cd6 | ||
|
ef01f4ace9 | ||
|
095d672417 | ||
|
bd599e5654 | ||
|
42fe3c05f8 | ||
|
425f1ecd75 | ||
|
994dfa5a0e | ||
|
d699d13ee0 | ||
|
bef2334577 | ||
|
420895d409 | ||
|
88a2b7b1b9 | ||
|
3b83b2fe24 | ||
|
89628c0309 | ||
|
afe5ca7b95 | ||
|
07545a5d7e | ||
|
05b6d15030 | ||
|
6069087b61 | ||
|
a5229737cb | ||
|
5641cddb58 | ||
|
4bdaaed2fc | ||
|
62138237b7 | ||
|
5f7d1988fb | ||
|
e1ba65beac | ||
|
a6b9ec8955 | ||
|
e4585853a3 | ||
|
842116b55a | ||
|
52b7874942 | ||
|
c0fca459ca | ||
|
ee490bb346 | ||
|
b1aee17987 | ||
|
d6695d894b | ||
|
6fcd457a88 | ||
|
014e152703 | ||
|
65a0c346ec | ||
|
ed4aaf94c3 | ||
|
cc838a058a | ||
|
fcd04c0a60 | ||
|
d251d400f3 | ||
|
4c10fb38dd | ||
|
061062bcfb | ||
|
cc51a72443 | ||
|
78ee0aaf6a | ||
|
9a6c945474 | ||
|
5e9d507f57 | ||
|
ba2c3a03ec | ||
|
0827a1e80c | ||
|
6f5485d997 | ||
|
8eb12a9517 | ||
|
7ac4d5687b | ||
|
e2a3c5d5c8 | ||
|
d9a1565cdd | ||
|
9e395ffacd | ||
|
2bdc6797ba | ||
|
f808f5d4b2 | ||
|
26a5d6eb8c | ||
|
f61201afc6 | ||
|
619410f83c | ||
|
006ff6b6ea | ||
|
04b57c7f47 | ||
|
7a9f815117 | ||
|
3360e1a376 | ||
|
61265090f9 | ||
|
e46081020c | ||
|
0e6f7b6620 | ||
|
a98994ac2c | ||
|
e69f26ef82 | ||
|
e0699a9802 | ||
|
d0715e7c6b | ||
|
baf6d26b4d | ||
|
72923c49f2 | ||
|
37e1f8ee6f | ||
|
4311a815d3 | ||
|
8a9d70391b | ||
|
9b56fcc53c | ||
|
9bb05f6f77 | ||
|
c099a78d5f | ||
|
eb42e85b47 | ||
|
e9e17c4d2b | ||
|
02c8d28f88 | ||
|
af4d4721c8 | ||
|
d8ca8959e3 | ||
|
9631aa114b | ||
|
96872573a1 | ||
|
863e57096f | ||
|
8446365e00 | ||
|
32e641c176 | ||
|
ed9627d875 | ||
|
c40e4a14b4 | ||
|
89c1dcda9a | ||
|
bac7aaa722 | ||
|
c5d1621d4d | ||
|
45f7332bdf | ||
|
d7b210427b | ||
|
aa29dc5cc8 | ||
|
28c73fe98e | ||
|
42131fbf5f | ||
|
cd9a7b3303 | ||
|
89dc15ffd1 | ||
|
f2e654cb0f | ||
|
55c2d275ca | ||
|
b55ee3f334 | ||
|
cdb9007675 | ||
|
bd30811e78 | ||
|
e7652b8b19 | ||
|
e653b6b9c5 | ||
|
5da14cbeb1 | ||
|
118dbfd37d | ||
|
e48581489d | ||
|
ef352b86ce | ||
|
f0cc7a75aa | ||
|
08337c9859 | ||
|
33a502b655 | ||
|
c04b8a9d84 | ||
|
d661692366 | ||
|
c39f366247 | ||
|
f38a49c75d | ||
|
f48793e3d6 | ||
|
259a4e9e0b | ||
|
760449b9f6 | ||
|
3c272212d6 | ||
|
98779a6515 | ||
|
8cf4e0d9dd | ||
|
23c404076f | ||
|
a2b9d8cbc6 | ||
|
93762818c3 | ||
|
2dbb08b2cf | ||
|
1bde7e873e | ||
|
b3557417ee | ||
|
79c4ef7638 | ||
|
64bded1f43 | ||
|
a94113a338 | ||
|
c0672fb776 | ||
|
eda0812259 | ||
|
16b31caff7 | ||
|
1893302e18 | ||
|
dacf792e7a | ||
|
537125df52 | ||
|
1a837d96a9 | ||
|
ba4ce90f27 | ||
|
a05a86c2ae | ||
|
5ebd263a6d | ||
|
69127d8af2 | ||
|
a4d0eccd47 | ||
|
59ef3d2978 | ||
|
f0fab5310b | ||
|
714719ca7e | ||
|
e709b84347 | ||
|
97b1aba452 | ||
|
c7a6b86311 | ||
|
fa660fcc32 | ||
|
2537604d31 | ||
|
5aed68e51a | ||
|
c65bc696cc | ||
|
dd8129e003 | ||
|
83b9224e34 | ||
|
82c445dc5e | ||
|
c36ba33084 | ||
|
0d150d0b3a | ||
|
362e65ff52 | ||
|
70958f63ff | ||
|
f473474425 | ||
|
0742cffd0d | ||
|
3618fbec59 | ||
|
a7a58ab7a2 | ||
|
1ca4d27015 | ||
|
f4834b5e78 | ||
|
14476af917 | ||
|
3f49d07f9c | ||
|
b33a9b286b | ||
|
4722b6d87b | ||
|
62015362d9 | ||
|
a1644e8f72 | ||
|
14dcd1e1e2 | ||
|
6000c80ba4 | ||
|
bb1a3d32c4 | ||
|
78636d69e1 | ||
|
2715f9b175 | ||
|
fdb1129429 | ||
|
d5d452485d | ||
|
7f26bbf14f | ||
|
d05029b3b8 | ||
|
c9911c883d | ||
|
a280532113 | ||
|
608b31111f | ||
|
d1bf0de479 | ||
|
0a39636e75 | ||
|
513e4d243f | ||
|
c9dc19b168 | ||
|
cc97b945e0 | ||
|
1b64d170e8 | ||
|
51c526fedf | ||
|
aa93e6e6b3 | ||
|
d9fed02c33 | ||
|
4604367352 | ||
|
2e2b106bdb | ||
|
14661cfd90 | ||
|
921416289d | ||
|
a3714c4362 | ||
|
123453e062 | ||
|
239b62f06e | ||
|
9ec1fe7de5 | ||
|
1d194e6ec5 | ||
|
541a534a24 | ||
|
8d4dda68b7 | ||
|
df3f043998 | ||
|
65925dfd88 | ||
|
b5f462b1cb | ||
|
62c92bec97 | ||
|
a35b5a1cd7 | ||
|
000b280f2d | ||
|
860fdce745 | ||
|
dad84b4d8c | ||
|
e8f57615b1 | ||
|
eb3153ba3f | ||
|
1aebc968bb | ||
|
8b7e0bdea5 | ||
|
e6dc165fbc | ||
|
ad17ddcda5 | ||
|
34f5ee46ca | ||
|
77c08988ed | ||
|
d017139bd9 | ||
|
ae22d9ad92 | ||
|
62aeb7486c | ||
|
345c65c4e7 | ||
|
f31784881f | ||
|
e22149ae20 | ||
|
a0edc09d2e | ||
|
a51cc6ca37 | ||
|
2cd37644f0 | ||
|
727c9e8bc5 | ||
|
2593d17f33 | ||
|
02b7fc56ff | ||
|
d12f4e136f | ||
|
111cdcaa10 | ||
|
2dfab471b6 | ||
|
daae3f99a7 | ||
|
8ecd452d5d | ||
|
ff535bbd9f | ||
|
fdbed04f77 | ||
|
759c56a070 | ||
|
65467925e2 | ||
|
456c378a32 | ||
|
ee5632ac65 | ||
|
e586829352 | ||
|
4af037d535 | ||
|
6a054ec15d | ||
|
717e92756c | ||
|
77cf7f08a8 | ||
|
27edf980d2 | ||
|
5b688d1f7b | ||
|
32d984f238 | ||
|
f477463f0a | ||
|
aaaca521b6 | ||
|
df6a63c867 | ||
|
ede3f57d79 | ||
|
4fe191fbb6 | ||
|
96a27ce630 | ||
|
f297c821ef | ||
|
53c20f5a18 | ||
|
812eafe73d | ||
|
ec61c64a1b | ||
|
ce5f8c5a44 | ||
|
0beceaaa17 | ||
|
d53c72cecc | ||
|
d6e8a632f8 | ||
|
477621d80d | ||
|
8f189af767 | ||
|
47e9f053d4 | ||
|
3b4615f675 | ||
|
1173ffb919 | ||
|
d0d6221ece | ||
|
a02973e681 | ||
|
bc6a752523 | ||
|
fb616c5958 | ||
|
c06502975c | ||
|
48c11c27e7 | ||
|
105f7c57f3 | ||
|
2e49c859c3 | ||
|
14970758ab | ||
|
bdb170146d | ||
|
4eb2c15553 | ||
|
aaa17e4337 | ||
|
8e095536a0 | ||
|
6433b4ce3e | ||
|
c902e40bde | ||
|
1c1f9d62d3 | ||
|
4a08b85194 | ||
|
a1c11f2de0 | ||
|
ad7764fdb9 | ||
|
df1a7af12d | ||
|
9d89de3db7 | ||
|
b91b895b20 | ||
|
fca1ff5909 | ||
|
6dc2621f78 | ||
|
d0f0a4156a | ||
|
713670cfc7 | ||
|
ec9fa66c76 | ||
|
2a1e21e44b | ||
|
562f87c589 | ||
|
da91236576 | ||
|
841c783256 | ||
|
ac49855234 | ||
|
d996885778 | ||
|
0e81b4ed6f | ||
|
e3ac45cbd7 | ||
|
4614c71c70 | ||
|
c7c2136720 | ||
|
5093e32c64 | ||
|
1ab4f59861 | ||
|
83a093cf75 | ||
|
f84cb192c6 | ||
|
d4e5cf9261 | ||
|
7a320c96e9 | ||
|
1fbef4e9ed | ||
|
bef33831ec | ||
|
f2e47f06d8 | ||
|
3f28c0ad8e | ||
|
8a2f166eb1 | ||
|
2b756feeb9 | ||
|
11dec3313b | ||
|
e0ee8a02ad | ||
|
bd9dc6580c | ||
|
e66031c4be | ||
|
052632bcc9 | ||
|
15fd6cbe76 | ||
|
27266107a6 | ||
|
fe61a0ec2d | ||
|
70c79e5159 | ||
|
953d9c9985 | ||
|
11367b94dc | ||
|
e9e783c46f | ||
|
935876546d | ||
|
27afa29d2d | ||
|
8cbd7f458b | ||
|
20a43ee73d | ||
|
fcd9b81687 | ||
|
b9cb0ed516 | ||
|
cad253f393 | ||
|
041b260eef | ||
|
ad916045f5 | ||
|
e371269fc4 | ||
|
bc8f1daa39 | ||
|
05b8e52660 | ||
|
4e2dd38c78 | ||
|
294aab2990 | ||
|
d908f5fc87 | ||
|
250ecc878f | ||
|
a2b636f9df | ||
|
c338b8b808 | ||
|
a2f465b2a5 | ||
|
88dd43f629 | ||
|
f2c5e57447 | ||
|
a10203eb8c | ||
|
1967e820dd | ||
|
b3342876e1 | ||
|
a7ecdde675 | ||
|
d177158652 | ||
|
408ccc4e6c | ||
|
c5fa0113e6 | ||
|
4f6c8b7a46 | ||
|
fd2a3e8eeb | ||
|
7e4e5c9eff | ||
|
b5fd7f15ba | ||
|
5b5799b1c6 | ||
|
c7ec49e322 | ||
|
387ca531f4 | ||
|
a3219a5f87 | ||
|
b947b12f83 | ||
|
6e02b80971 | ||
|
28d25d732c | ||
|
817df09921 | ||
|
759ba6a65c | ||
|
27a247b65a | ||
|
eb6048b3e2 | ||
|
f4ce7f458c | ||
|
59a8e5efec | ||
|
6805e43933 | ||
|
d410bab8a2 | ||
|
34061856fd | ||
|
a244e15e16 | ||
|
63e72c5f46 | ||
|
a19e76511e | ||
|
beb77d3950 | ||
|
54adee33ca | ||
|
6bb69a0afc | ||
|
f6c8256ae6 | ||
|
e29c1d4444 | ||
|
670c4d4aeb | ||
|
92628c2873 | ||
|
fd07266f14 | ||
|
58b0d8dfa4 | ||
|
300882e249 | ||
|
49d618f56e | ||
|
d8d944fdee | ||
|
e0615c8283 | ||
|
63f52f3f7a | ||
|
63002f497e | ||
|
15463be849 | ||
|
fa0b49df28 | ||
|
e635d1a95d | ||
|
0bf3e6ee8b | ||
|
f376ca0d51 | ||
|
f07c4668f8 | ||
|
0f151e4754 | ||
|
10d2ca7d64 | ||
|
21130834a8 | ||
|
cdea1995a3 | ||
|
7d7095bb20 | ||
|
e4b69820d0 | ||
|
1d89cadbeb | ||
|
3612c2d875 | ||
|
474fcd713b | ||
|
977ea9d3a6 | ||
|
482b2671fa | ||
|
565fbec415 | ||
|
6c69cd001b | ||
|
d671f42a85 | ||
|
bef0094790 | ||
|
200b5a1b46 | ||
|
ae734cdc1c | ||
|
b95d0547cc | ||
|
0c9807e3ca | ||
|
c277a9373d | ||
|
d2b6d40be7 | ||
|
f5804f97eb | ||
|
6c1148ac2e | ||
|
c15c33633c | ||
|
b96da09e9e | ||
|
cb97e11eb6 | ||
|
40db7100fd | ||
|
b2ac98a4dc | ||
|
7ad8fc1fb5 | ||
|
23133772ab | ||
|
c85d98f674 | ||
|
e9babccb4c | ||
|
de27d412e6 | ||
|
d425698242 | ||
|
2c77284ff3 | ||
|
ba8d10a22a | ||
|
68eb665fa1 | ||
|
cd3285d70f | ||
|
1924993d32 | ||
|
5331108f22 | ||
|
fabd9f5dc6 | ||
|
f6890bcd47 | ||
|
8994c292e1 | ||
|
223cfc6b07 | ||
|
94b077748a | ||
|
e2a3c68d63 | ||
|
bb5aa2f81d | ||
|
7f0ac99712 | ||
|
1f978920c6 | ||
|
ce56099d57 | ||
|
c0f3ade8d1 | ||
|
c4fec9dbc6 | ||
|
bc634da52f | ||
|
84e26cf1dc | ||
|
be6c02e9b4 | ||
|
e1f7f39995 | ||
|
909a031ba0 | ||
|
56ac4b42d5 | ||
|
cfbc4bf9ad | ||
|
97eb31b03c | ||
|
62f82f9611 | ||
|
be6eac0d70 | ||
|
98c9da20c2 | ||
|
3bc6d9c5c2 | ||
|
ec51176ab5 | ||
|
6dbf51084a | ||
|
ce60ada2a7 | ||
|
7493298a1b | ||
|
c24036cff8 | ||
|
6958990414 | ||
|
e12b002592 | ||
|
b411b7f515 | ||
|
03a71df471 | ||
|
fe70f26760 | ||
|
e5beae52c2 | ||
|
c901376094 | ||
|
5db5a6197e | ||
|
51ba2990f2 | ||
|
5d5b2bcb65 | ||
|
6d41c05a01 | ||
|
70a1323015 | ||
|
43478677cd | ||
|
388256fcf9 | ||
|
ed6fb3b5b0 | ||
|
c1798ce6f0 | ||
|
c15c8b0997 | ||
|
052fc62199 | ||
|
ffad716b6d | ||
|
9330ccea31 | ||
|
3437b8d04b | ||
|
cbfb808c80 | ||
|
343289a0fe | ||
|
00fbdad493 | ||
|
27fde588b9 | ||
|
f88e9ccde7 | ||
|
dc1f515c1a | ||
|
e51c90c5ca | ||
|
76995a8802 | ||
|
cd24b5c6ee | ||
|
f91115931a | ||
|
132a8cd923 | ||
|
b3649fc570 | ||
|
f46220a419 | ||
|
174bf08361 | ||
|
da8535c77a | ||
|
6ee6564901 | ||
|
20354da4d8 | ||
|
87985cdade | ||
|
460b287fd1 | ||
|
cc974f47d6 | ||
|
da39ae0dd8 | ||
|
e9628e2b1e | ||
|
da68c794dd | ||
|
b55fed2a08 | ||
|
1532d42829 | ||
|
5205446d21 | ||
|
584a9fc6d6 | ||
|
d44d91cf2e | ||
|
67b721c132 | ||
|
f751c5ab6d | ||
|
4971e450d2 | ||
|
485ce19491 | ||
|
4ec6069e1f | ||
|
c920734ff8 | ||
|
173c9cdda7 | ||
|
d61b18d172 | ||
|
1e60d52fda | ||
|
9b2b36afa5 | ||
|
7564cb9a76 | ||
|
6d5d39d310 | ||
|
e44a45eb59 | ||
|
142a2e2f2a | ||
|
cbf3784022 | ||
|
6fd89157bc | ||
|
5bfcec9697 | ||
|
096fec621c | ||
|
a68478e219 | ||
|
b33771eacd | ||
|
d8f3d78c33 | ||
|
d9b7943af2 | ||
|
bf67882423 | ||
|
068e68d025 | ||
|
cc19c18f53 | ||
|
ae6086a144 | ||
|
a41ad14564 | ||
|
0c9ec3a8b3 | ||
|
3558f21d8b | ||
|
4e7086b534 | ||
|
1feae720f7 | ||
|
f4621a2b8c | ||
|
2e64371de1 | ||
|
b0d15da6b9 | ||
|
68dd86ec37 | ||
|
3b73799b12 | ||
|
47c9c83ad5 | ||
|
759473c579 | ||
|
fbfef03f77 | ||
|
17b3c36099 | ||
|
b334316bdf | ||
|
8073832356 | ||
|
2e2e2687b8 | ||
|
1349660538 | ||
|
0bbd57a11f | ||
|
a107d42e24 | ||
|
29a2130105 | ||
|
d66dc2edfb | ||
|
a42d15eec5 | ||
|
5c970df9ee | ||
|
5ee9c6a548 | ||
|
fd98401079 | ||
|
f1f2a10a10 | ||
|
95f7e8a4bf | ||
|
baa15b470f | ||
|
25d58b0c0f | ||
|
87303a19bd | ||
|
09eb9a7194 | ||
|
a635ea5321 | ||
|
90cbdb28fa | ||
|
246aea556b | ||
|
488522aa90 | ||
|
ff0e3925f8 | ||
|
d91b495203 | ||
|
4c88221a4f | ||
|
ce47aa7f3c | ||
|
b60fb53dcd | ||
|
727b01d6c3 | ||
|
345265fc9a | ||
|
677a277735 | ||
|
084b2ec797 | ||
|
88ed301585 | ||
|
3140b9cac3 | ||
|
771554cf93 | ||
|
3fff0864e2 | ||
|
0d5567d87f | ||
|
3b5167ee50 | ||
|
13ce381fc4 | ||
|
b6b135923c | ||
|
078ab6c5e6 | ||
|
53f9826a26 | ||
|
993e28fc06 | ||
|
ce2f0c23c5 | ||
|
96292b9f3b | ||
|
ab6d83eb9e | ||
|
d2867535bc | ||
|
eb5541d686 | ||
|
d4c90d7aee | ||
|
d53c9ea042 | ||
|
38b0211ea1 | ||
|
356bf6eb69 | ||
|
c811f79fd3 | ||
|
f42508971e | ||
|
f6a2ff7523 | ||
|
0923fd4084 | ||
|
5a435aec57 | ||
|
470901ade4 | ||
|
ca2f3ea951 | ||
|
fd22d4b730 | ||
|
201e48eabf | ||
|
e656d9168e | ||
|
53c6dd0c89 | ||
|
d179e76083 | ||
|
994f12e3ba | ||
|
6d21aa99f6 | ||
|
6243cdd4a1 | ||
|
1150cf249d | ||
|
35167f05b1 | ||
|
54aee25089 | ||
|
a66f19da4b | ||
|
913774a41d | ||
|
958df16454 | ||
|
538dbb9c11 | ||
|
f5e4905ee7 | ||
|
939750c8e9 | ||
|
6af3f2ea37 | ||
|
1dff7781c8 | ||
|
5ca3cf693c | ||
|
b922467970 | ||
|
99424d5126 | ||
|
ae6de1e431 | ||
|
8eea021744 | ||
|
360cc9f194 | ||
|
4e1a73f676 | ||
|
03be69ae57 | ||
|
676374607a | ||
|
dd93154c54 | ||
|
ecb1ed6238 | ||
|
6f71d2c35b | ||
|
04854b56f9 | ||
|
727db384a1 | ||
|
27504e1ed4 | ||
|
69be9c6038 | ||
|
0df92eb193 | ||
|
91bd36ef0f | ||
|
f0236cc69a | ||
|
a3e6f5ab04 | ||
|
fc3f9b6c1a | ||
|
175e49e805 | ||
|
1c004b06bd | ||
|
9f17443a52 | ||
|
b1adccd017 | ||
|
1fa62dfd4d | ||
|
ea71297187 | ||
|
479c404618 | ||
|
e7ed9ded86 | ||
|
52e7c37403 | ||
|
7a5e0e6ddb | ||
|
dd1058c2c9 | ||
|
758cff5fc7 | ||
|
c1c71f6a31 | ||
|
76ef29f2dd | ||
|
490c714454 | ||
|
4394a61358 | ||
|
ce450badcf | ||
|
9950edec7d | ||
|
3b3453f8b2 | ||
|
3a14e7ccb3 | ||
|
f7fa45836b | ||
|
98ddad8bad | ||
|
829c072a11 | ||
|
820e185ce6 | ||
|
b5753a9220 | ||
|
c2fe6d4286 | ||
|
628a1410ff | ||
|
40c0e06c75 | ||
|
c04f4e4488 | ||
|
11a2845159 | ||
|
8d6d416594 | ||
|
679b7eba66 | ||
|
78048582f0 | ||
|
aeb9a3eebc | ||
|
fcfe1a4751 | ||
|
3ca52d7bb3 | ||
|
bfd9640e03 | ||
|
81cfada54d | ||
|
b6f1806dc6 | ||
|
66d4cf6c73 | ||
|
4857b49dcc | ||
|
34152de210 | ||
|
cf19ec7670 | ||
|
b29d03c065 | ||
|
aed04af273 | ||
|
a40edbf838 | ||
|
712beae181 | ||
|
4720f12c18 | ||
|
fc30b5057f | ||
|
2d9927f333 | ||
|
801227e736 | ||
|
7115695508 | ||
|
00dbf94ace | ||
|
66ee64e6ca | ||
|
1284af89e1 | ||
|
fad08a57d2 | ||
|
125414e080 | ||
|
2520f7ad20 | ||
|
09aa389704 | ||
|
06682d705d | ||
|
647defcab0 | ||
|
34106f7295 | ||
|
f601304f24 | ||
|
9e7f40efd3 | ||
|
b1f7205fb8 | ||
|
9ddaa6b605 | ||
|
bd8f737493 | ||
|
0b19f9ddb9 | ||
|
27baf280e5 | ||
|
4c27d6e134 | ||
|
927db8469d | ||
|
b8daf61be4 | ||
|
4a505597bf | ||
|
8183f44463 | ||
|
1107b29c36 | ||
|
d4621f8b8b | ||
|
a7baa4e5c4 | ||
|
5f0bad068b | ||
|
5424e4631b | ||
|
df027a1f3c | ||
|
a2b82e1f5d | ||
|
cc4454d497 | ||
|
0f558afed9 | ||
|
44e6166d4b | ||
|
3843e1b9ec | ||
|
9955ac9dd5 | ||
|
34504850e3 | ||
|
4dac0bbaf4 | ||
|
d88716d16d |
803 changed files with 125769 additions and 34883 deletions
75
.env.dev.example
Normal file
75
.env.dev.example
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG=true
|
||||||
|
USE_HTTPS=false
|
||||||
|
|
||||||
|
DOMAIN=your.domain.here
|
||||||
|
#EMAIL=your@email.here
|
||||||
|
|
||||||
|
# Used for deciding which editions to prefer
|
||||||
|
DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
|
## Leave unset to allow all hosts
|
||||||
|
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
|
||||||
|
|
||||||
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
|
PGPORT=5432
|
||||||
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
|
POSTGRES_USER=fedireads
|
||||||
|
POSTGRES_DB=fedireads
|
||||||
|
POSTGRES_HOST=db
|
||||||
|
|
||||||
|
# Redis activity stream manager
|
||||||
|
MAX_STREAM_LENGTH=200
|
||||||
|
REDIS_ACTIVITY_HOST=redis_activity
|
||||||
|
REDIS_ACTIVITY_PORT=6379
|
||||||
|
#REDIS_ACTIVITY_PASSWORD=redispassword345
|
||||||
|
|
||||||
|
# Redis as celery broker
|
||||||
|
REDIS_BROKER_PORT=6379
|
||||||
|
#REDIS_BROKER_PASSWORD=redispassword123
|
||||||
|
|
||||||
|
FLOWER_PORT=8888
|
||||||
|
#FLOWER_USER=mouse
|
||||||
|
#FLOWER_PASSWORD=changeme
|
||||||
|
|
||||||
|
EMAIL_HOST="smtp.mailgun.org"
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_HOST_USER=mail@your.domain.here
|
||||||
|
EMAIL_HOST_PASSWORD=emailpassword123
|
||||||
|
EMAIL_USE_TLS=true
|
||||||
|
EMAIL_USE_SSL=false
|
||||||
|
|
||||||
|
# Thumbnails Generation
|
||||||
|
ENABLE_THUMBNAIL_GENERATION=false
|
||||||
|
|
||||||
|
# S3 configuration
|
||||||
|
USE_S3=false
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
# Commented are example values if you use a non-AWS, S3-compatible service
|
||||||
|
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
|
||||||
|
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
|
||||||
|
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
|
||||||
|
|
||||||
|
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
|
||||||
|
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
|
||||||
|
# AWS_S3_REGION_NAME=None # "fr-par"
|
||||||
|
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
|
||||||
|
|
||||||
|
|
||||||
|
# Preview image generation can be computing and storage intensive
|
||||||
|
# ENABLE_PREVIEW_IMAGES=True
|
||||||
|
|
||||||
|
# Specify RGB tuple or RGB hex strings,
|
||||||
|
# or use_dominant_color_light / use_dominant_color_dark
|
||||||
|
PREVIEW_BG_COLOR=use_dominant_color_light
|
||||||
|
# Change to #FFF if you use use_dominant_color_dark
|
||||||
|
PREVIEW_TEXT_COLOR="#363636"
|
||||||
|
PREVIEW_IMG_WIDTH=1200
|
||||||
|
PREVIEW_IMG_HEIGHT=630
|
||||||
|
PREVIEW_DEFAULT_COVER_COLOR="#002549"
|
|
@ -8,8 +8,6 @@ USE_HTTPS=true
|
||||||
DOMAIN=your.domain.here
|
DOMAIN=your.domain.here
|
||||||
EMAIL=your@email.here
|
EMAIL=your@email.here
|
||||||
|
|
||||||
# Instance defualt language (see options at bookwyrm/settings.py "LANGUAGES"
|
|
||||||
LANGUAGE_CODE="en-us"
|
|
||||||
# Used for deciding which editions to prefer
|
# Used for deciding which editions to prefer
|
||||||
DEFAULT_LANGUAGE="English"
|
DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
|
@ -18,7 +16,6 @@ DEFAULT_LANGUAGE="English"
|
||||||
|
|
||||||
MEDIA_ROOT=images/
|
MEDIA_ROOT=images/
|
||||||
|
|
||||||
# Database configuration
|
|
||||||
PGPORT=5432
|
PGPORT=5432
|
||||||
POSTGRES_PASSWORD=securedbypassword123
|
POSTGRES_PASSWORD=securedbypassword123
|
||||||
POSTGRES_USER=fedireads
|
POSTGRES_USER=fedireads
|
||||||
|
@ -30,34 +27,21 @@ MAX_STREAM_LENGTH=200
|
||||||
REDIS_ACTIVITY_HOST=redis_activity
|
REDIS_ACTIVITY_HOST=redis_activity
|
||||||
REDIS_ACTIVITY_PORT=6379
|
REDIS_ACTIVITY_PORT=6379
|
||||||
REDIS_ACTIVITY_PASSWORD=redispassword345
|
REDIS_ACTIVITY_PASSWORD=redispassword345
|
||||||
# Optional, use a different redis database (defaults to 0)
|
|
||||||
# REDIS_ACTIVITY_DB_INDEX=0
|
|
||||||
|
|
||||||
# Redis as celery broker
|
# Redis as celery broker
|
||||||
REDIS_BROKER_PORT=6379
|
REDIS_BROKER_PORT=6379
|
||||||
REDIS_BROKER_PASSWORD=redispassword123
|
REDIS_BROKER_PASSWORD=redispassword123
|
||||||
# Optional, use a different redis database (defaults to 0)
|
|
||||||
# REDIS_BROKER_DB_INDEX=0
|
|
||||||
|
|
||||||
# Monitoring for celery
|
|
||||||
FLOWER_PORT=8888
|
FLOWER_PORT=8888
|
||||||
FLOWER_USER=admin
|
FLOWER_USER=mouse
|
||||||
FLOWER_PASSWORD=changeme
|
FLOWER_PASSWORD=changeme
|
||||||
|
|
||||||
# Email config
|
EMAIL_HOST="smtp.mailgun.org"
|
||||||
EMAIL_HOST=smtp.mailgun.org
|
|
||||||
EMAIL_PORT=587
|
EMAIL_PORT=587
|
||||||
EMAIL_HOST_USER=mail@your.domain.here
|
EMAIL_HOST_USER=mail@your.domain.here
|
||||||
EMAIL_HOST_PASSWORD=emailpassword123
|
EMAIL_HOST_PASSWORD=emailpassword123
|
||||||
EMAIL_USE_TLS=true
|
EMAIL_USE_TLS=true
|
||||||
EMAIL_USE_SSL=false
|
EMAIL_USE_SSL=false
|
||||||
EMAIL_SENDER_NAME=admin
|
|
||||||
# defaults to DOMAIN
|
|
||||||
EMAIL_SENDER_DOMAIN=
|
|
||||||
|
|
||||||
# Query timeouts
|
|
||||||
SEARCH_TIMEOUT=15
|
|
||||||
QUERY_TIMEOUT=5
|
|
||||||
|
|
||||||
# Thumbnails Generation
|
# Thumbnails Generation
|
||||||
ENABLE_THUMBNAIL_GENERATION=false
|
ENABLE_THUMBNAIL_GENERATION=false
|
||||||
|
@ -85,26 +69,7 @@ AWS_SECRET_ACCESS_KEY=
|
||||||
# or use_dominant_color_light / use_dominant_color_dark
|
# or use_dominant_color_light / use_dominant_color_dark
|
||||||
PREVIEW_BG_COLOR=use_dominant_color_light
|
PREVIEW_BG_COLOR=use_dominant_color_light
|
||||||
# Change to #FFF if you use use_dominant_color_dark
|
# Change to #FFF if you use use_dominant_color_dark
|
||||||
PREVIEW_TEXT_COLOR=#363636
|
PREVIEW_TEXT_COLOR="#363636"
|
||||||
PREVIEW_IMG_WIDTH=1200
|
PREVIEW_IMG_WIDTH=1200
|
||||||
PREVIEW_IMG_HEIGHT=630
|
PREVIEW_IMG_HEIGHT=630
|
||||||
PREVIEW_DEFAULT_COVER_COLOR=#002549
|
PREVIEW_DEFAULT_COVER_COLOR="#002549"
|
||||||
|
|
||||||
# Below are example keys if you want to enable automatically
|
|
||||||
# sending telemetry to an OTLP-compatible service. Many of
|
|
||||||
# the main monitoring apps have OLTP collectors, including
|
|
||||||
# NewRelic, DataDog, and Honeycomb.io - consult their
|
|
||||||
# documentation for setup instructions, and what exactly to
|
|
||||||
# put below!
|
|
||||||
#
|
|
||||||
# Service name is an arbitrary tag that is attached to any
|
|
||||||
# data sent, used to distinguish different sources. Useful
|
|
||||||
# for sending prod and dev metrics to the same place and
|
|
||||||
# keeping them separate, for instance!
|
|
||||||
|
|
||||||
# API endpoint for your provider
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
|
||||||
# Any headers required, usually authentication info
|
|
||||||
OTEL_EXPORTER_OTLP_HEADERS=
|
|
||||||
# Service name to identify your app
|
|
||||||
OTEL_SERVICE_NAME=
|
|
2
.github/workflows/django-tests.yml
vendored
2
.github/workflows/django-tests.yml
vendored
|
@ -46,8 +46,6 @@ jobs:
|
||||||
POSTGRES_HOST: 127.0.0.1
|
POSTGRES_HOST: 127.0.0.1
|
||||||
CELERY_BROKER: ""
|
CELERY_BROKER: ""
|
||||||
REDIS_BROKER_PORT: 6379
|
REDIS_BROKER_PORT: 6379
|
||||||
REDIS_BROKER_PASSWORD: beep
|
|
||||||
USE_DUMMY_CACHE: true
|
|
||||||
FLOWER_PORT: 8888
|
FLOWER_PORT: 8888
|
||||||
EMAIL_HOST: "smtp.mailgun.org"
|
EMAIL_HOST: "smtp.mailgun.org"
|
||||||
EMAIL_PORT: 587
|
EMAIL_PORT: 587
|
||||||
|
|
13
.github/workflows/lint-frontend.yaml
vendored
13
.github/workflows/lint-frontend.yaml
vendored
|
@ -1,5 +1,5 @@
|
||||||
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||||
name: Lint Frontend (run `./bw-dev stylelint` to fix css errors)
|
name: Lint Frontend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -8,7 +8,7 @@ on:
|
||||||
- '.github/workflows/**'
|
- '.github/workflows/**'
|
||||||
- 'static/**'
|
- 'static/**'
|
||||||
- '.eslintrc'
|
- '.eslintrc'
|
||||||
- '.stylelintrc.js'
|
- '.stylelintrc'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, ci, frontend ]
|
branches: [ main, ci, frontend ]
|
||||||
|
|
||||||
|
@ -22,16 +22,17 @@ jobs:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: npm install stylelint stylelint-config-recommended stylelint-config-standard stylelint-order eslint
|
run: yarn
|
||||||
|
|
||||||
# See .stylelintignore for files that are not linted.
|
# See .stylelintignore for files that are not linted.
|
||||||
- name: Run stylelint
|
- name: Run stylelint
|
||||||
run: >
|
run: >
|
||||||
npx stylelint bookwyrm/static/css/*.scss bookwyrm/static/css/bookwyrm/**/*.scss \
|
yarn stylelint bookwyrm/static/**/*.css \
|
||||||
--config dev-tools/.stylelintrc.js
|
--report-needless-disables \
|
||||||
|
--report-invalid-scope-disables
|
||||||
|
|
||||||
# See .eslintignore for files that are not linted.
|
# See .eslintignore for files that are not linted.
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: >
|
run: >
|
||||||
npx eslint bookwyrm/static \
|
yarn eslint bookwyrm/static \
|
||||||
--ext .js,.jsx,.ts,.tsx
|
--ext .js,.jsx,.ts,.tsx
|
||||||
|
|
21
.github/workflows/lint-global.yaml
vendored
Normal file
21
.github/workflows/lint-global.yaml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||||
|
name: Lint project globally
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, ci ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, ci ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint with EditorConfig.
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: EditorConfig
|
||||||
|
uses: greut/eclint-action@v0
|
23
.github/workflows/prettier.yaml
vendored
23
.github/workflows/prettier.yaml
vendored
|
@ -1,23 +0,0 @@
|
||||||
# @url https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
|
||||||
name: JavaScript Prettier (run ./bw-dev prettier to fix)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint with Prettier
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it.
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install modules
|
|
||||||
run: npm install prettier
|
|
||||||
|
|
||||||
- name: Run Prettier
|
|
||||||
run: npx prettier --check bookwyrm/static/js/*.js
|
|
3
.github/workflows/pylint.yml
vendored
3
.github/workflows/pylint.yml
vendored
|
@ -21,7 +21,8 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
pip install pylint
|
||||||
- name: Analysing the code with pylint
|
- name: Analysing the code with pylint
|
||||||
run: |
|
run: |
|
||||||
pylint bookwyrm/
|
pylint bookwyrm/ --ignore=migrations,tests --disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801
|
||||||
|
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -4,7 +4,6 @@
|
||||||
*.swp
|
*.swp
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
.local
|
.local
|
||||||
/nginx/nginx.conf
|
|
||||||
|
|
||||||
# VSCode
|
# VSCode
|
||||||
/.vscode
|
/.vscode
|
||||||
|
@ -16,9 +15,6 @@
|
||||||
# BookWyrm
|
# BookWyrm
|
||||||
.env
|
.env
|
||||||
/images/
|
/images/
|
||||||
bookwyrm/static/css/bookwyrm.css
|
|
||||||
bookwyrm/static/css/themes/
|
|
||||||
!bookwyrm/static/css/themes/bookwyrm-*.scss
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
.coverage
|
.coverage
|
||||||
|
@ -27,9 +23,7 @@ bookwyrm/static/css/themes/
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
#Node tools
|
#Node tools
|
||||||
node_modules/
|
/node_modules/
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
#nginx
|
#nginx
|
||||||
nginx/default.conf
|
nginx/default.conf
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
**/vendor/*
|
|
|
@ -1,6 +0,0 @@
|
||||||
[MAIN]
|
|
||||||
ignore=migrations
|
|
||||||
load-plugins=pylint.extensions.no_self_use
|
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
|
||||||
disable=E1101,E1135,E1136,R0903,R0901,R0902,W0707,W0511,W0406,R0401,R0801,C3001
|
|
17
.stylelintrc.js
Normal file
17
.stylelintrc.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/* global module */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"extends": "stylelint-config-standard",
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
"stylelint-order"
|
||||||
|
],
|
||||||
|
|
||||||
|
"rules": {
|
||||||
|
"order/order": [
|
||||||
|
"custom-properties",
|
||||||
|
"declarations"
|
||||||
|
],
|
||||||
|
"indentation": 4
|
||||||
|
}
|
||||||
|
};
|
|
@ -6,7 +6,6 @@ RUN mkdir /app /app/static /app/images
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean
|
|
||||||
|
|
||||||
COPY requirements.txt /app/
|
COPY requirements.txt /app/
|
||||||
RUN pip install -r requirements.txt --no-cache-dir
|
RUN pip install -r requirements.txt --no-cache-dir
|
||||||
|
RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean
|
||||||
|
|
18
README.md
18
README.md
|
@ -9,18 +9,21 @@ Social reading and reviewing, decentralized with ActivityPub
|
||||||
- [What it is and isn't](#what-it-is-and-isnt)
|
- [What it is and isn't](#what-it-is-and-isnt)
|
||||||
- [The role of federation](#the-role-of-federation)
|
- [The role of federation](#the-role-of-federation)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Set up BookWyrm](#set-up-bookwyrm)
|
- [Book data](#book-data)
|
||||||
|
- [Set up Bookwyrm](#set-up-bookwyrm)
|
||||||
|
|
||||||
## Joining BookWyrm
|
## Joining BookWyrm
|
||||||
If you'd like to join an instance, you can check out the [instances](https://joinbookwyrm.com/instances/) list.
|
BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://docs.joinbookwyrm.com/instances.html) list.
|
||||||
|
|
||||||
|
You can request an invite by entering your email address at https://bookwyrm.social.
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
See [contributing](https://docs.joinbookwyrm.com/contributing.html) for code, translation or monetary contributions.
|
See [contributing](https://docs.joinbookwyrm.com/how-to-contribute.html) for code, translation or monetary contributions.
|
||||||
|
|
||||||
## About BookWyrm
|
## About BookWyrm
|
||||||
### What it is and isn't
|
### What it is and isn't
|
||||||
BookWyrm is a platform for social reading. You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
|
BookWyrm is a platform for social reading! You can use it to track what you're reading, review books, and follow your friends. It isn't primarily meant for cataloguing or as a data-source for books, but it does do both of those things to some degree.
|
||||||
|
|
||||||
### The role of federation
|
### The role of federation
|
||||||
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
|
BookWyrm is built on [ActivityPub](http://activitypub.rocks/). With ActivityPub, it inter-operates with different instances of BookWyrm, and other ActivityPub compliant services, like Mastodon. This means you can run an instance for your book club, and still follow your friend who posts on a server devoted to 20th century Russian speculative fiction. It also means that your friend on mastodon can read and comment on a book review that you post on your BookWyrm instance.
|
||||||
|
@ -75,5 +78,8 @@ Deployment
|
||||||
- [Nginx](https://nginx.org/en/) HTTP server
|
- [Nginx](https://nginx.org/en/) HTTP server
|
||||||
|
|
||||||
|
|
||||||
## Set up BookWyrm
|
## Book data
|
||||||
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up BookWyrm in a [developer environment](https://docs.joinbookwyrm.com/install-dev.html) or [production](https://docs.joinbookwyrm.com/install-prod.html).
|
The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written.
|
||||||
|
|
||||||
|
## Set up Bookwyrm
|
||||||
|
The [documentation website](https://docs.joinbookwyrm.com/) has instruction on how to set up Bookwyrm in a [developer environment](https://docs.joinbookwyrm.com/developer-environment.html) or [production](https://docs.joinbookwyrm.com/installing-in-production.html).
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
""" basics for an activitypub serializer """
|
""" basics for an activitypub serializer """
|
||||||
from dataclasses import dataclass, fields, MISSING
|
from dataclasses import dataclass, fields, MISSING
|
||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
|
@ -9,8 +8,6 @@ from django.db import IntegrityError, transaction
|
||||||
from bookwyrm.connectors import ConnectorException, get_data
|
from bookwyrm.connectors import ConnectorException, get_data
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ActivitySerializerError(ValueError):
|
class ActivitySerializerError(ValueError):
|
||||||
"""routine problems serializing activitypub json"""
|
"""routine problems serializing activitypub json"""
|
||||||
|
@ -23,6 +20,22 @@ class ActivityEncoder(JSONEncoder):
|
||||||
return o.__dict__
|
return o.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Link:
|
||||||
|
"""for tagging a book in a status"""
|
||||||
|
|
||||||
|
href: str
|
||||||
|
name: str
|
||||||
|
type: str = "Link"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mention(Link):
|
||||||
|
"""a subtype of Link for mentioning an actor"""
|
||||||
|
|
||||||
|
type: str = "Mention"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
class Signature:
|
class Signature:
|
||||||
|
@ -42,12 +55,12 @@ def naive_parse(activity_objects, activity_json, serializer=None):
|
||||||
activity_json["type"] = "PublicKey"
|
activity_json["type"] = "PublicKey"
|
||||||
|
|
||||||
activity_type = activity_json.get("type")
|
activity_type = activity_json.get("type")
|
||||||
if activity_type in ["Question", "Article"]:
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
serializer = activity_objects[activity_type]
|
serializer = activity_objects[activity_type]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
# we know this exists and that we can't handle it
|
# we know this exists and that we can't handle it
|
||||||
|
if activity_type in ["Question"]:
|
||||||
|
return None
|
||||||
raise ActivitySerializerError(err)
|
raise ActivitySerializerError(err)
|
||||||
|
|
||||||
return serializer(activity_objects=activity_objects, **activity_json)
|
return serializer(activity_objects=activity_objects, **activity_json)
|
||||||
|
@ -68,7 +81,7 @@ class ActivityObject:
|
||||||
try:
|
try:
|
||||||
value = kwargs[field.name]
|
value = kwargs[field.name]
|
||||||
if value in (None, MISSING, {}):
|
if value in (None, MISSING, {}):
|
||||||
raise KeyError("Missing required field", field.name)
|
raise KeyError()
|
||||||
try:
|
try:
|
||||||
is_subclass = issubclass(field.type, ActivityObject)
|
is_subclass = issubclass(field.type, ActivityObject)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -185,9 +198,8 @@ class ActivityObject:
|
||||||
)
|
)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def serialize(self, **kwargs):
|
def serialize(self):
|
||||||
"""convert to dictionary with context attr"""
|
"""convert to dictionary with context attr"""
|
||||||
omit = kwargs.get("omit", ())
|
|
||||||
data = self.__dict__.copy()
|
data = self.__dict__.copy()
|
||||||
# recursively serialize
|
# recursively serialize
|
||||||
for (k, v) in data.items():
|
for (k, v) in data.items():
|
||||||
|
@ -196,8 +208,7 @@ class ActivityObject:
|
||||||
data[k] = v.serialize()
|
data[k] = v.serialize()
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
data = {k: v for (k, v) in data.items() if v is not None and k not in omit}
|
data = {k: v for (k, v) in data.items() if v is not None}
|
||||||
if "@context" not in omit:
|
|
||||||
data["@context"] = "https://www.w3.org/ns/activitystreams"
|
data["@context"] = "https://www.w3.org/ns/activitystreams"
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -211,6 +222,7 @@ def set_related_field(
|
||||||
model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True)
|
model = apps.get_model(f"bookwyrm.{model_name}", require_ready=True)
|
||||||
origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True)
|
origin_model = apps.get_model(f"bookwyrm.{origin_model_name}", require_ready=True)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
existing = model.find_existing_by_remote_id(data)
|
existing = model.find_existing_by_remote_id(data)
|
||||||
if existing:
|
if existing:
|
||||||
|
@ -229,8 +241,10 @@ def set_related_field(
|
||||||
# edition.parentWork = instance, for example
|
# edition.parentWork = instance, for example
|
||||||
model_field = getattr(model, related_field_name)
|
model_field = getattr(model, related_field_name)
|
||||||
if hasattr(model_field, "activitypub_field"):
|
if hasattr(model_field, "activitypub_field"):
|
||||||
setattr(activity, getattr(model_field, "activitypub_field"), instance.remote_id)
|
setattr(
|
||||||
item = activity.to_model(model=model)
|
activity, getattr(model_field, "activitypub_field"), instance.remote_id
|
||||||
|
)
|
||||||
|
item = activity.to_model()
|
||||||
|
|
||||||
# if the related field isn't serialized (attachments on Status), then
|
# if the related field isn't serialized (attachments on Status), then
|
||||||
# we have to set it post-creation
|
# we have to set it post-creation
|
||||||
|
@ -271,9 +285,9 @@ def resolve_remote_id(
|
||||||
try:
|
try:
|
||||||
data = get_data(remote_id)
|
data = get_data(remote_id)
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
logger.exception("Could not connect to host for remote_id: %s", remote_id)
|
raise ActivitySerializerError(
|
||||||
return None
|
f"Could not connect to host for remote_id: {remote_id}"
|
||||||
|
)
|
||||||
# determine the model implicitly, if not provided
|
# determine the model implicitly, if not provided
|
||||||
# or if it's a model with subclasses like Status, check again
|
# or if it's a model with subclasses like Status, check again
|
||||||
if not model or hasattr(model.objects, "select_subclasses"):
|
if not model or hasattr(model.objects, "select_subclasses"):
|
||||||
|
@ -290,28 +304,3 @@ def resolve_remote_id(
|
||||||
|
|
||||||
# if we're refreshing, "result" will be set and we'll update it
|
# if we're refreshing, "result" will be set and we'll update it
|
||||||
return item.to_model(model=model, instance=result, save=save)
|
return item.to_model(model=model, instance=result, save=save)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
|
||||||
class Link(ActivityObject):
|
|
||||||
"""for tagging a book in a status"""
|
|
||||||
|
|
||||||
href: str
|
|
||||||
name: str = None
|
|
||||||
mediaType: str = None
|
|
||||||
id: str = None
|
|
||||||
attributedTo: str = None
|
|
||||||
availability: str = None
|
|
||||||
type: str = "Link"
|
|
||||||
|
|
||||||
def serialize(self, **kwargs):
|
|
||||||
"""remove fields"""
|
|
||||||
omit = ("id", "type", "@context")
|
|
||||||
return super().serialize(omit=omit)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
|
||||||
class Mention(Link):
|
|
||||||
"""a subtype of Link for mentioning an actor"""
|
|
||||||
|
|
||||||
type: str = "Mention"
|
|
||||||
|
|
|
@ -16,12 +16,7 @@ class BookData(ActivityObject):
|
||||||
librarythingKey: str = None
|
librarythingKey: str = None
|
||||||
goodreadsKey: str = None
|
goodreadsKey: str = None
|
||||||
bnfId: str = None
|
bnfId: str = None
|
||||||
viaf: str = None
|
|
||||||
wikidata: str = None
|
|
||||||
asin: str = None
|
|
||||||
lastEditedBy: str = None
|
lastEditedBy: str = None
|
||||||
links: List[str] = field(default_factory=lambda: [])
|
|
||||||
fileLinks: List[str] = field(default_factory=lambda: [])
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
@ -30,8 +25,8 @@ class Book(BookData):
|
||||||
"""serializes an edition or work, abstract"""
|
"""serializes an edition or work, abstract"""
|
||||||
|
|
||||||
title: str
|
title: str
|
||||||
sortTitle: str = None
|
sortTitle: str = ""
|
||||||
subtitle: str = None
|
subtitle: str = ""
|
||||||
description: str = ""
|
description: str = ""
|
||||||
languages: List[str] = field(default_factory=lambda: [])
|
languages: List[str] = field(default_factory=lambda: [])
|
||||||
series: str = ""
|
series: str = ""
|
||||||
|
@ -56,6 +51,7 @@ class Edition(Book):
|
||||||
isbn10: str = ""
|
isbn10: str = ""
|
||||||
isbn13: str = ""
|
isbn13: str = ""
|
||||||
oclcNumber: str = ""
|
oclcNumber: str = ""
|
||||||
|
asin: str = ""
|
||||||
pages: int = None
|
pages: int = None
|
||||||
physicalFormat: str = ""
|
physicalFormat: str = ""
|
||||||
physicalFormatDetail: str = ""
|
physicalFormatDetail: str = ""
|
||||||
|
|
|
@ -35,7 +35,6 @@ class Note(ActivityObject):
|
||||||
tag: List[Link] = field(default_factory=lambda: [])
|
tag: List[Link] = field(default_factory=lambda: [])
|
||||||
attachment: List[Document] = field(default_factory=lambda: [])
|
attachment: List[Document] = field(default_factory=lambda: [])
|
||||||
sensitive: bool = False
|
sensitive: bool = False
|
||||||
updated: str = None
|
|
||||||
type: str = "Note"
|
type: str = "Note"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,6 @@ class PublicKey(ActivityObject):
|
||||||
publicKeyPem: str
|
publicKeyPem: str
|
||||||
type: str = "PublicKey"
|
type: str = "PublicKey"
|
||||||
|
|
||||||
def serialize(self, **kwargs):
|
|
||||||
"""remove fields"""
|
|
||||||
omit = ("type", "@context")
|
|
||||||
return super().serialize(omit=omit)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
|
@ -39,5 +34,4 @@ class Person(ActivityObject):
|
||||||
bookwyrmUser: bool = False
|
bookwyrmUser: bool = False
|
||||||
manuallyApprovesFollowers: str = False
|
manuallyApprovesFollowers: str = False
|
||||||
discoverable: str = False
|
discoverable: str = False
|
||||||
hideFollows: str = False
|
|
||||||
type: str = "Person"
|
type: str = "Person"
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Create(Verb):
|
||||||
class Delete(Verb):
|
class Delete(Verb):
|
||||||
"""Create activity"""
|
"""Create activity"""
|
||||||
|
|
||||||
to: List[str] = field(default_factory=lambda: [])
|
to: List[str]
|
||||||
cc: List[str] = field(default_factory=lambda: [])
|
cc: List[str] = field(default_factory=lambda: [])
|
||||||
type: str = "Delete"
|
type: str = "Delete"
|
||||||
|
|
||||||
|
@ -69,8 +69,7 @@ class Update(Verb):
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
"""update a model instance from the dataclass"""
|
"""update a model instance from the dataclass"""
|
||||||
if not self.object:
|
if self.object:
|
||||||
return
|
|
||||||
self.object.to_model(allow_create=False)
|
self.object.to_model(allow_create=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -137,8 +136,8 @@ class Accept(Verb):
|
||||||
type: str = "Accept"
|
type: str = "Accept"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
"""accept a request"""
|
"""find and remove the activity object"""
|
||||||
obj = self.object.to_model(save=False, allow_create=True)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.accept()
|
obj.accept()
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,7 +149,7 @@ class Reject(Verb):
|
||||||
type: str = "Reject"
|
type: str = "Reject"
|
||||||
|
|
||||||
def action(self):
|
def action(self):
|
||||||
"""reject a follow request"""
|
"""find and remove the activity object"""
|
||||||
obj = self.object.to_model(save=False, allow_create=False)
|
obj = self.object.to_model(save=False, allow_create=False)
|
||||||
obj.reject()
|
obj.reject()
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.redis_store import RedisStore, r
|
from bookwyrm.redis_store import RedisStore, r
|
||||||
from bookwyrm.tasks import app, LOW, MEDIUM, HIGH
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
|
|
||||||
class ActivityStream(RedisStore):
|
class ActivityStream(RedisStore):
|
||||||
|
@ -22,11 +22,6 @@ class ActivityStream(RedisStore):
|
||||||
stream_id = self.stream_id(user)
|
stream_id = self.stream_id(user)
|
||||||
return f"{stream_id}-unread"
|
return f"{stream_id}-unread"
|
||||||
|
|
||||||
def unread_by_status_type_id(self, user):
|
|
||||||
"""the redis key for this user's unread count for this stream"""
|
|
||||||
stream_id = self.stream_id(user)
|
|
||||||
return f"{stream_id}-unread-by-type"
|
|
||||||
|
|
||||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
def get_rank(self, obj): # pylint: disable=no-self-use
|
||||||
"""statuses are sorted by date published"""
|
"""statuses are sorted by date published"""
|
||||||
return obj.published_date.timestamp()
|
return obj.published_date.timestamp()
|
||||||
|
@ -40,10 +35,6 @@ class ActivityStream(RedisStore):
|
||||||
for user in self.get_audience(status):
|
for user in self.get_audience(status):
|
||||||
# add to the unread status count
|
# add to the unread status count
|
||||||
pipeline.incr(self.unread_id(user))
|
pipeline.incr(self.unread_id(user))
|
||||||
# add to the unread status count for status type
|
|
||||||
pipeline.hincrby(
|
|
||||||
self.unread_by_status_type_id(user), get_status_type(status), 1
|
|
||||||
)
|
|
||||||
|
|
||||||
# and go!
|
# and go!
|
||||||
pipeline.execute()
|
pipeline.execute()
|
||||||
|
@ -64,7 +55,6 @@ class ActivityStream(RedisStore):
|
||||||
"""load the statuses to be displayed"""
|
"""load the statuses to be displayed"""
|
||||||
# clear unreads for this feed
|
# clear unreads for this feed
|
||||||
r.set(self.unread_id(user), 0)
|
r.set(self.unread_id(user), 0)
|
||||||
r.delete(self.unread_by_status_type_id(user))
|
|
||||||
|
|
||||||
statuses = self.get_store(self.stream_id(user))
|
statuses = self.get_store(self.stream_id(user))
|
||||||
return (
|
return (
|
||||||
|
@ -85,14 +75,6 @@ class ActivityStream(RedisStore):
|
||||||
"""get the unread status count for this user's feed"""
|
"""get the unread status count for this user's feed"""
|
||||||
return int(r.get(self.unread_id(user)) or 0)
|
return int(r.get(self.unread_id(user)) or 0)
|
||||||
|
|
||||||
def get_unread_count_by_status_type(self, user):
|
|
||||||
"""get the unread status count for this user's feed's status types"""
|
|
||||||
status_types = r.hgetall(self.unread_by_status_type_id(user))
|
|
||||||
return {
|
|
||||||
str(key.decode("utf-8")): int(value) or 0
|
|
||||||
for key, value in status_types.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
def populate_streams(self, user):
|
def populate_streams(self, user):
|
||||||
"""go from zero to a timeline"""
|
"""go from zero to a timeline"""
|
||||||
self.populate_store(self.stream_id(user))
|
self.populate_store(self.stream_id(user))
|
||||||
|
@ -295,19 +277,7 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
|
||||||
|
|
||||||
def add_status_on_create_command(sender, instance, created):
|
def add_status_on_create_command(sender, instance, created):
|
||||||
"""runs this code only after the database commit completes"""
|
"""runs this code only after the database commit completes"""
|
||||||
priority = HIGH
|
add_status_task.delay(instance.id, increment_unread=created)
|
||||||
# check if this is an old status, de-prioritize if so
|
|
||||||
# (this will happen if federation is very slow, or, more expectedly, on csv import)
|
|
||||||
if instance.published_date < timezone.now() - timedelta(
|
|
||||||
days=1
|
|
||||||
) or instance.created_date < instance.published_date - timedelta(days=1):
|
|
||||||
priority = LOW
|
|
||||||
|
|
||||||
add_status_task.apply_async(
|
|
||||||
args=(instance.id,),
|
|
||||||
kwargs={"increment_unread": created},
|
|
||||||
queue=priority,
|
|
||||||
)
|
|
||||||
|
|
||||||
if sender == models.Boost:
|
if sender == models.Boost:
|
||||||
handle_boost_task.delay(instance.id)
|
handle_boost_task.delay(instance.id)
|
||||||
|
@ -398,15 +368,9 @@ def populate_streams_on_account_create(sender, instance, created, *args, **kwarg
|
||||||
"""build a user's feeds when they join"""
|
"""build a user's feeds when they join"""
|
||||||
if not created or not instance.local:
|
if not created or not instance.local:
|
||||||
return
|
return
|
||||||
transaction.on_commit(
|
|
||||||
lambda: populate_streams_on_account_create_command(instance.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def populate_streams_on_account_create_command(instance_id):
|
|
||||||
"""wait for the transaction to complete"""
|
|
||||||
for stream in streams:
|
for stream in streams:
|
||||||
populate_stream_task.delay(stream, instance_id)
|
populate_stream_task.delay(stream, instance.id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.pre_save, sender=models.ShelfBook)
|
@receiver(signals.pre_save, sender=models.ShelfBook)
|
||||||
|
@ -445,7 +409,7 @@ def remove_statuses_on_unshelve(sender, instance, *args, **kwargs):
|
||||||
# ---- TASKS
|
# ---- TASKS
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=LOW)
|
@app.task(queue="low_priority")
|
||||||
def add_book_statuses_task(user_id, book_id):
|
def add_book_statuses_task(user_id, book_id):
|
||||||
"""add statuses related to a book on shelve"""
|
"""add statuses related to a book on shelve"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -453,7 +417,7 @@ def add_book_statuses_task(user_id, book_id):
|
||||||
BooksStream().add_book_statuses(user, book)
|
BooksStream().add_book_statuses(user, book)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=LOW)
|
@app.task(queue="low_priority")
|
||||||
def remove_book_statuses_task(user_id, book_id):
|
def remove_book_statuses_task(user_id, book_id):
|
||||||
"""remove statuses about a book from a user's books feed"""
|
"""remove statuses about a book from a user's books feed"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -461,7 +425,7 @@ def remove_book_statuses_task(user_id, book_id):
|
||||||
BooksStream().remove_book_statuses(user, book)
|
BooksStream().remove_book_statuses(user, book)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
@app.task(queue="medium_priority")
|
||||||
def populate_stream_task(stream, user_id):
|
def populate_stream_task(stream, user_id):
|
||||||
"""background task for populating an empty activitystream"""
|
"""background task for populating an empty activitystream"""
|
||||||
user = models.User.objects.get(id=user_id)
|
user = models.User.objects.get(id=user_id)
|
||||||
|
@ -469,7 +433,7 @@ def populate_stream_task(stream, user_id):
|
||||||
stream.populate_streams(user)
|
stream.populate_streams(user)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
@app.task(queue="medium_priority")
|
||||||
def remove_status_task(status_ids):
|
def remove_status_task(status_ids):
|
||||||
"""remove a status from any stream it might be in"""
|
"""remove a status from any stream it might be in"""
|
||||||
# this can take an id or a list of ids
|
# this can take an id or a list of ids
|
||||||
|
@ -482,10 +446,10 @@ def remove_status_task(status_ids):
|
||||||
stream.remove_object_from_related_stores(status)
|
stream.remove_object_from_related_stores(status)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=HIGH)
|
@app.task(queue="high_priority")
|
||||||
def add_status_task(status_id, increment_unread=False):
|
def add_status_task(status_id, increment_unread=False):
|
||||||
"""add a status to any stream it should be in"""
|
"""add a status to any stream it should be in"""
|
||||||
status = models.Status.objects.select_subclasses().get(id=status_id)
|
status = models.Status.objects.get(id=status_id)
|
||||||
# we don't want to tick the unread count for csv import statuses, idk how better
|
# we don't want to tick the unread count for csv import statuses, idk how better
|
||||||
# to check than just to see if the states is more than a few days old
|
# to check than just to see if the states is more than a few days old
|
||||||
if status.created_date < timezone.now() - timedelta(days=2):
|
if status.created_date < timezone.now() - timedelta(days=2):
|
||||||
|
@ -494,7 +458,7 @@ def add_status_task(status_id, increment_unread=False):
|
||||||
stream.add_status(status, increment_unread=increment_unread)
|
stream.add_status(status, increment_unread=increment_unread)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
@app.task(queue="medium_priority")
|
||||||
def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
|
def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
"""remove all statuses by a user from a viewer's stream"""
|
"""remove all statuses by a user from a viewer's stream"""
|
||||||
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
||||||
|
@ -504,7 +468,7 @@ def remove_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
stream.remove_user_statuses(viewer, user)
|
stream.remove_user_statuses(viewer, user)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
@app.task(queue="medium_priority")
|
||||||
def add_user_statuses_task(viewer_id, user_id, stream_list=None):
|
def add_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
"""add all statuses by a user to a viewer's stream"""
|
"""add all statuses by a user to a viewer's stream"""
|
||||||
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
stream_list = [streams[s] for s in stream_list] if stream_list else streams.values()
|
||||||
|
@ -514,7 +478,7 @@ def add_user_statuses_task(viewer_id, user_id, stream_list=None):
|
||||||
stream.add_user_statuses(viewer, user)
|
stream.add_user_statuses(viewer, user)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
@app.task(queue="medium_priority")
|
||||||
def handle_boost_task(boost_id):
|
def handle_boost_task(boost_id):
|
||||||
"""remove the original post and other, earlier boosts"""
|
"""remove the original post and other, earlier boosts"""
|
||||||
instance = models.Status.objects.get(id=boost_id)
|
instance = models.Status.objects.get(id=boost_id)
|
||||||
|
@ -532,20 +496,3 @@ def handle_boost_task(boost_id):
|
||||||
stream.remove_object_from_related_stores(boosted, stores=audience)
|
stream.remove_object_from_related_stores(boosted, stores=audience)
|
||||||
for status in old_versions:
|
for status in old_versions:
|
||||||
stream.remove_object_from_related_stores(status, stores=audience)
|
stream.remove_object_from_related_stores(status, stores=audience)
|
||||||
|
|
||||||
|
|
||||||
def get_status_type(status):
|
|
||||||
"""return status type even for boosted statuses"""
|
|
||||||
status_type = status.status_type.lower()
|
|
||||||
|
|
||||||
# Check if current status is a boost
|
|
||||||
if hasattr(status, "boost"):
|
|
||||||
# Act in accordance of your findings
|
|
||||||
if hasattr(status.boost.boosted_status, "review"):
|
|
||||||
status_type = "review"
|
|
||||||
if hasattr(status.boost.boosted_status, "comment"):
|
|
||||||
status_type = "comment"
|
|
||||||
if hasattr(status.boost.boosted_status, "quotation"):
|
|
||||||
status_type = "quotation"
|
|
||||||
|
|
||||||
return status_type
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
"""Do further startup configuration and initialization"""
|
|
||||||
import os
|
|
||||||
import urllib
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
from bookwyrm import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def download_file(url, destination):
|
|
||||||
"""Downloads a file to the given path"""
|
|
||||||
try:
|
|
||||||
# Ensure our destination directory exists
|
|
||||||
os.makedirs(os.path.dirname(destination))
|
|
||||||
with urllib.request.urlopen(url) as stream:
|
|
||||||
with open(destination, "b+w") as outfile:
|
|
||||||
outfile.write(stream.read())
|
|
||||||
except (urllib.error.HTTPError, urllib.error.URLError):
|
|
||||||
logger.info("Failed to download file %s", url)
|
|
||||||
except OSError:
|
|
||||||
logger.info("Couldn't open font file %s for writing", destination)
|
|
||||||
except: # pylint: disable=bare-except
|
|
||||||
logger.info("Unknown error in file download")
|
|
||||||
|
|
||||||
|
|
||||||
class BookwyrmConfig(AppConfig):
|
|
||||||
"""Handles additional configuration"""
|
|
||||||
|
|
||||||
name = "bookwyrm"
|
|
||||||
verbose_name = "BookWyrm"
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def ready(self):
|
|
||||||
"""set up OTLP and preview image files, if desired"""
|
|
||||||
if settings.OTEL_EXPORTER_OTLP_ENDPOINT:
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from bookwyrm.telemetry import open_telemetry
|
|
||||||
|
|
||||||
open_telemetry.instrumentDjango()
|
|
||||||
|
|
||||||
if settings.ENABLE_PREVIEW_IMAGES and settings.FONTS:
|
|
||||||
# Download any fonts that we don't have yet
|
|
||||||
logger.debug("Downloading fonts..")
|
|
||||||
for name, config in settings.FONTS.items():
|
|
||||||
font_path = os.path.join(
|
|
||||||
settings.FONT_DIR, config["directory"], config["filename"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if "url" in config and not os.path.exists(font_path):
|
|
||||||
logger.info("Just a sec, downloading %s", name)
|
|
||||||
download_file(config["url"], font_path)
|
|
|
@ -82,8 +82,6 @@ def search_identifiers(query, *filters, return_first=False):
|
||||||
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
||||||
).distinct()
|
).distinct()
|
||||||
if results.count() <= 1:
|
if results.count() <= 1:
|
||||||
if return_first:
|
|
||||||
return results.first()
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# when there are multiple editions of the same work, pick the default.
|
# when there are multiple editions of the same work, pick the default.
|
||||||
|
@ -126,7 +124,6 @@ def search_title_author(query, min_confidence, *filters, return_first=False):
|
||||||
result = default
|
result = default
|
||||||
else:
|
else:
|
||||||
result = editions.first()
|
result = editions.first()
|
||||||
|
|
||||||
if return_first:
|
if return_first:
|
||||||
return result
|
return result
|
||||||
list_results.append(result)
|
list_results.append(result)
|
||||||
|
@ -148,8 +145,8 @@ class SearchResult:
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
# pylint: disable=consider-using-f-string
|
# pylint: disable=consider-using-f-string
|
||||||
return "<SearchResult key={!r} title={!r} author={!r} confidence={!r}>".format(
|
return "<SearchResult key={!r} title={!r} author={!r}>".format(
|
||||||
self.key, self.title, self.author, self.confidence
|
self.key, self.title, self.author
|
||||||
)
|
)
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
""" functionality outline for a book data connector """
|
""" functionality outline for a book data connector """
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import imghdr
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from bookwyrm import activitypub, models, settings
|
from bookwyrm import activitypub, models, settings
|
||||||
from .connector_manager import load_more_data, ConnectorException, raise_not_valid_url
|
from .connector_manager import load_more_data, ConnectorException
|
||||||
from .format_mappings import format_mappings
|
from .format_mappings import format_mappings
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,34 +35,61 @@ class AbstractMinimalConnector(ABC):
|
||||||
for field in self_fields:
|
for field in self_fields:
|
||||||
setattr(self, field, getattr(info, field))
|
setattr(self, field, getattr(info, field))
|
||||||
|
|
||||||
def get_search_url(self, query):
|
def search(self, query, min_confidence=None, timeout=5):
|
||||||
"""format the query url"""
|
"""free text search"""
|
||||||
# Check if the query resembles an ISBN
|
params = {}
|
||||||
if maybe_isbn(query) and self.isbn_search_url and self.isbn_search_url != "":
|
if min_confidence:
|
||||||
return f"{self.isbn_search_url}{query}"
|
params["min_confidence"] = min_confidence
|
||||||
|
|
||||||
# NOTE: previously, we tried searching isbn and if that produces no results,
|
data = self.get_search_data(
|
||||||
# searched as free text. This, instead, only searches isbn if it's isbn-y
|
f"{self.search_url}{query}",
|
||||||
return f"{self.search_url}{query}"
|
params=params,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results = []
|
||||||
|
|
||||||
def process_search_response(self, query, data, min_confidence):
|
for doc in self.parse_search_data(data)[:10]:
|
||||||
"""Format the search results based on the formt of the query"""
|
results.append(self.format_search_result(doc))
|
||||||
if maybe_isbn(query):
|
return results
|
||||||
return list(self.parse_isbn_search_data(data))[:10]
|
|
||||||
return list(self.parse_search_data(data, min_confidence))[:10]
|
def isbn_search(self, query):
|
||||||
|
"""isbn search"""
|
||||||
|
params = {}
|
||||||
|
data = self.get_search_data(
|
||||||
|
f"{self.isbn_search_url}{query}",
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# this shouldn't be returning mutliple results, but just in case
|
||||||
|
for doc in self.parse_isbn_search_data(data)[:10]:
|
||||||
|
results.append(self.format_isbn_search_result(doc))
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_search_data(self, remote_id, **kwargs): # pylint: disable=no-self-use
|
||||||
|
"""this allows connectors to override the default behavior"""
|
||||||
|
return get_data(remote_id, **kwargs)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
"""pull up a book record by whatever means possible"""
|
"""pull up a book record by whatever means possible"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse_search_data(self, data, min_confidence):
|
def parse_search_data(self, data):
|
||||||
"""turn the result json from a search into a list"""
|
"""turn the result json from a search into a list"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def format_search_result(self, search_result):
|
||||||
|
"""create a SearchResult obj from json"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
"""turn the result json from a search into a list"""
|
"""turn the result json from a search into a list"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
"""create a SearchResult obj from json"""
|
||||||
|
|
||||||
|
|
||||||
class AbstractConnector(AbstractMinimalConnector):
|
class AbstractConnector(AbstractMinimalConnector):
|
||||||
"""generic book data connector"""
|
"""generic book data connector"""
|
||||||
|
@ -87,7 +111,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
return existing.default_edition
|
return existing.default_edition
|
||||||
return existing
|
return existing
|
||||||
|
|
||||||
# load the json data from the remote data source
|
# load the json
|
||||||
data = self.get_book_data(remote_id)
|
data = self.get_book_data(remote_id)
|
||||||
if self.is_work_data(data):
|
if self.is_work_data(data):
|
||||||
try:
|
try:
|
||||||
|
@ -102,7 +126,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
try:
|
try:
|
||||||
work_data = self.get_work_from_edition_data(data)
|
work_data = self.get_work_from_edition_data(data)
|
||||||
except (KeyError, ConnectorException) as err:
|
except (KeyError, ConnectorException) as err:
|
||||||
logger.info(err)
|
logger.exception(err)
|
||||||
work_data = data
|
work_data = data
|
||||||
|
|
||||||
if not work_data or not edition_data:
|
if not work_data or not edition_data:
|
||||||
|
@ -126,34 +150,24 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
"""this allows connectors to override the default behavior"""
|
"""this allows connectors to override the default behavior"""
|
||||||
return get_data(remote_id)
|
return get_data(remote_id)
|
||||||
|
|
||||||
def create_edition_from_data(self, work, edition_data, instance=None):
|
def create_edition_from_data(self, work, edition_data):
|
||||||
"""if we already have the work, we're ready"""
|
"""if we already have the work, we're ready"""
|
||||||
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
|
||||||
mapped_data["work"] = work.remote_id
|
mapped_data["work"] = work.remote_id
|
||||||
edition_activity = activitypub.Edition(**mapped_data)
|
edition_activity = activitypub.Edition(**mapped_data)
|
||||||
edition = edition_activity.to_model(
|
edition = edition_activity.to_model(model=models.Edition, overwrite=False)
|
||||||
model=models.Edition, overwrite=False, instance=instance
|
|
||||||
)
|
|
||||||
|
|
||||||
# if we're updating an existing instance, we don't need to load authors
|
|
||||||
if instance:
|
|
||||||
return edition
|
|
||||||
|
|
||||||
if not edition.connector:
|
|
||||||
edition.connector = self.connector
|
edition.connector = self.connector
|
||||||
edition.save(broadcast=False, update_fields=["connector"])
|
edition.save()
|
||||||
|
|
||||||
for author in self.get_authors_from_data(edition_data):
|
for author in self.get_authors_from_data(edition_data):
|
||||||
edition.authors.add(author)
|
edition.authors.add(author)
|
||||||
# use the authors from the work if none are found for the edition
|
|
||||||
if not edition.authors.exists() and work.authors.exists():
|
if not edition.authors.exists() and work.authors.exists():
|
||||||
edition.authors.set(work.authors.all())
|
edition.authors.set(work.authors.all())
|
||||||
|
|
||||||
return edition
|
return edition
|
||||||
|
|
||||||
def get_or_create_author(self, remote_id, instance=None):
|
def get_or_create_author(self, remote_id):
|
||||||
"""load that author"""
|
"""load that author"""
|
||||||
if not instance:
|
|
||||||
existing = models.Author.find_existing_by_remote_id(remote_id)
|
existing = models.Author.find_existing_by_remote_id(remote_id)
|
||||||
if existing:
|
if existing:
|
||||||
return existing
|
return existing
|
||||||
|
@ -167,24 +181,7 @@ class AbstractConnector(AbstractMinimalConnector):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# this will dedupe
|
# this will dedupe
|
||||||
return activity.to_model(
|
return activity.to_model(model=models.Author, overwrite=False)
|
||||||
model=models.Author, overwrite=False, instance=instance
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_remote_id_from_model(self, obj):
|
|
||||||
"""given the data stored, how can we look this up"""
|
|
||||||
return getattr(obj, getattr(self, "generated_remote_link_field"))
|
|
||||||
|
|
||||||
def update_author_from_remote(self, obj):
|
|
||||||
"""load the remote data from this connector and add it to an existing author"""
|
|
||||||
remote_id = self.get_remote_id_from_model(obj)
|
|
||||||
return self.get_or_create_author(remote_id, instance=obj)
|
|
||||||
|
|
||||||
def update_book_from_remote(self, obj):
|
|
||||||
"""load the remote data from this connector and add it to an existing book"""
|
|
||||||
remote_id = self.get_remote_id_from_model(obj)
|
|
||||||
data = self.get_book_data(remote_id)
|
|
||||||
return self.create_edition_from_data(obj.parent_work, data, instance=obj)
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
|
@ -223,38 +220,36 @@ def dict_from_mappings(data, mappings):
|
||||||
def get_data(url, params=None, timeout=10):
|
def get_data(url, params=None, timeout=10):
|
||||||
"""wrapper for request.get"""
|
"""wrapper for request.get"""
|
||||||
# check if the url is blocked
|
# check if the url is blocked
|
||||||
raise_not_valid_url(url)
|
if models.FederatedServer.is_blocked(url):
|
||||||
|
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
params=params,
|
params=params,
|
||||||
headers={ # pylint: disable=line-too-long
|
headers={
|
||||||
"Accept": (
|
"Accept": "application/json; charset=utf-8",
|
||||||
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
|
|
||||||
),
|
|
||||||
"User-Agent": settings.USER_AGENT,
|
"User-Agent": settings.USER_AGENT,
|
||||||
},
|
},
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
except RequestException as err:
|
except RequestException as err:
|
||||||
logger.info(err)
|
logger.exception(err)
|
||||||
raise ConnectorException(err)
|
raise ConnectorException()
|
||||||
|
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
raise ConnectorException()
|
raise ConnectorException()
|
||||||
try:
|
try:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
logger.info(err)
|
logger.exception(err)
|
||||||
raise ConnectorException(err)
|
raise ConnectorException()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def get_image(url, timeout=10):
|
def get_image(url, timeout=10):
|
||||||
"""wrapper for requesting an image"""
|
"""wrapper for requesting an image"""
|
||||||
raise_not_valid_url(url)
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
url,
|
url,
|
||||||
|
@ -264,19 +259,11 @@ def get_image(url, timeout=10):
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
except RequestException as err:
|
except RequestException as err:
|
||||||
logger.info(err)
|
logger.exception(err)
|
||||||
return None, None
|
return None
|
||||||
|
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
return None, None
|
return None
|
||||||
|
return resp
|
||||||
image_content = ContentFile(resp.content)
|
|
||||||
extension = imghdr.what(None, image_content.read())
|
|
||||||
if not extension:
|
|
||||||
logger.info("File requested was not an image: %s", url)
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
return image_content, extension
|
|
||||||
|
|
||||||
|
|
||||||
class Mapping:
|
class Mapping:
|
||||||
|
@ -320,9 +307,3 @@ def unique_physical_format(format_text):
|
||||||
# try a direct match, so saving this would be redundant
|
# try a direct match, so saving this would be redundant
|
||||||
return None
|
return None
|
||||||
return format_text
|
return format_text
|
||||||
|
|
||||||
|
|
||||||
def maybe_isbn(query):
|
|
||||||
"""check if a query looks like an isbn"""
|
|
||||||
isbn = re.sub(r"[\W_]", "", query) # removes filler characters
|
|
||||||
return len(isbn) in [10, 13] # ISBN10 or ISBN13
|
|
||||||
|
|
|
@ -10,12 +10,15 @@ class Connector(AbstractMinimalConnector):
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
return activitypub.resolve_remote_id(remote_id, model=models.Edition)
|
||||||
|
|
||||||
def parse_search_data(self, data, min_confidence):
|
def parse_search_data(self, data):
|
||||||
for search_result in data:
|
return data
|
||||||
|
|
||||||
|
def format_search_result(self, search_result):
|
||||||
search_result["connector"] = self
|
search_result["connector"] = self
|
||||||
yield SearchResult(**search_result)
|
return SearchResult(**search_result)
|
||||||
|
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
for search_result in data:
|
return data
|
||||||
search_result["connector"] = self
|
|
||||||
yield SearchResult(**search_result)
|
def format_isbn_search_result(self, search_result):
|
||||||
|
return self.format_search_result(search_result)
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
""" interface with whatever connectors the app has """
|
""" interface with whatever connectors the app has """
|
||||||
import asyncio
|
from datetime import datetime
|
||||||
import importlib
|
import importlib
|
||||||
import ipaddress
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
|
||||||
from bookwyrm import book_search, models
|
from bookwyrm import book_search, models
|
||||||
from bookwyrm.settings import SEARCH_TIMEOUT, USER_AGENT
|
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -22,85 +20,54 @@ class ConnectorException(HTTPError):
|
||||||
"""when the connector can't do what was asked"""
|
"""when the connector can't do what was asked"""
|
||||||
|
|
||||||
|
|
||||||
async def get_results(session, url, min_confidence, query, connector):
|
|
||||||
"""try this specific connector"""
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
headers = {
|
|
||||||
"Accept": (
|
|
||||||
'application/json, application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'
|
|
||||||
),
|
|
||||||
"User-Agent": USER_AGENT,
|
|
||||||
}
|
|
||||||
params = {"min_confidence": min_confidence}
|
|
||||||
try:
|
|
||||||
async with session.get(url, headers=headers, params=params) as response:
|
|
||||||
if not response.ok:
|
|
||||||
logger.info("Unable to connect to %s: %s", url, response.reason)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw_data = await response.json()
|
|
||||||
except aiohttp.client_exceptions.ContentTypeError as err:
|
|
||||||
logger.exception(err)
|
|
||||||
return
|
|
||||||
|
|
||||||
return {
|
|
||||||
"connector": connector,
|
|
||||||
"results": connector.process_search_response(
|
|
||||||
query, raw_data, min_confidence
|
|
||||||
),
|
|
||||||
}
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logger.info("Connection timed out for url: %s", url)
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
logger.exception(err)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_connector_search(query, items, min_confidence):
|
|
||||||
"""Try a number of requests simultaneously"""
|
|
||||||
timeout = aiohttp.ClientTimeout(total=SEARCH_TIMEOUT)
|
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
||||||
tasks = []
|
|
||||||
for url, connector in items:
|
|
||||||
tasks.append(
|
|
||||||
asyncio.ensure_future(
|
|
||||||
get_results(session, url, min_confidence, query, connector)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
results = await asyncio.gather(*tasks)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def search(query, min_confidence=0.1, return_first=False):
|
def search(query, min_confidence=0.1, return_first=False):
|
||||||
"""find books based on arbitary keywords"""
|
"""find books based on arbitary keywords"""
|
||||||
if not query:
|
if not query:
|
||||||
return []
|
return []
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
items = []
|
# Have we got a ISBN ?
|
||||||
for connector in get_connectors():
|
isbn = re.sub(r"[\W_]", "", query)
|
||||||
# get the search url from the connector before sending
|
maybe_isbn = len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||||
url = connector.get_search_url(query)
|
|
||||||
try:
|
|
||||||
raise_not_valid_url(url)
|
|
||||||
except ConnectorException:
|
|
||||||
# if this URL is invalid we should skip it and move on
|
|
||||||
logger.info("Request denied to blocked domain: %s", url)
|
|
||||||
continue
|
|
||||||
items.append((url, connector))
|
|
||||||
|
|
||||||
# load as many results as we can
|
timeout = 15
|
||||||
results = asyncio.run(async_connector_search(query, items, min_confidence))
|
start_time = datetime.now()
|
||||||
results = [r for r in results if r]
|
for connector in get_connectors():
|
||||||
|
result_set = None
|
||||||
|
if maybe_isbn and connector.isbn_search_url and connector.isbn_search_url != "":
|
||||||
|
# Search on ISBN
|
||||||
|
try:
|
||||||
|
result_set = connector.isbn_search(isbn)
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
logger.exception(err)
|
||||||
|
# if this fails, we can still try regular search
|
||||||
|
|
||||||
|
# if no isbn search results, we fallback to generic search
|
||||||
|
if not result_set:
|
||||||
|
try:
|
||||||
|
result_set = connector.search(query, min_confidence=min_confidence)
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
# we don't want *any* error to crash the whole search page
|
||||||
|
logger.exception(err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if return_first and result_set:
|
||||||
|
# if we found anything, return it
|
||||||
|
return result_set[0]
|
||||||
|
|
||||||
|
if result_set:
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"connector": connector,
|
||||||
|
"results": result_set,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (datetime.now() - start_time).seconds >= timeout:
|
||||||
|
break
|
||||||
|
|
||||||
if return_first:
|
if return_first:
|
||||||
# find the best result from all the responses and return that
|
return None
|
||||||
all_results = [r for con in results for r in con["results"]]
|
|
||||||
all_results = sorted(all_results, key=lambda r: r.confidence, reverse=True)
|
|
||||||
return all_results[0] if all_results else None
|
|
||||||
|
|
||||||
# failed requests will return None, so filter those out
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@ -166,20 +133,3 @@ def create_connector(sender, instance, created, *args, **kwargs):
|
||||||
"""create a connector to an external bookwyrm server"""
|
"""create a connector to an external bookwyrm server"""
|
||||||
if instance.application_type == "bookwyrm":
|
if instance.application_type == "bookwyrm":
|
||||||
get_or_create_connector(f"https://{instance.server_name}")
|
get_or_create_connector(f"https://{instance.server_name}")
|
||||||
|
|
||||||
|
|
||||||
def raise_not_valid_url(url):
|
|
||||||
"""do some basic reality checks on the url"""
|
|
||||||
parsed = urlparse(url)
|
|
||||||
if not parsed.scheme in ["http", "https"]:
|
|
||||||
raise ConnectorException("Invalid scheme: ", url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
ipaddress.ip_address(parsed.netloc)
|
|
||||||
raise ConnectorException("Provided url is an IP address: ", url)
|
|
||||||
except ValueError:
|
|
||||||
# it's not an IP address, which is good
|
|
||||||
pass
|
|
||||||
|
|
||||||
if models.FederatedServer.is_blocked(url):
|
|
||||||
raise ConnectorException(f"Attempting to load data from blocked url: {url}")
|
|
||||||
|
|
|
@ -11,8 +11,6 @@ from .connector_manager import ConnectorException
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
"""instantiate a connector for inventaire"""
|
"""instantiate a connector for inventaire"""
|
||||||
|
|
||||||
generated_remote_link_field = "inventaire_id"
|
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ class Connector(AbstractConnector):
|
||||||
extracted = list(data.get("entities").values())
|
extracted = list(data.get("entities").values())
|
||||||
try:
|
try:
|
||||||
data = extracted[0]
|
data = extracted[0]
|
||||||
except (KeyError, IndexError):
|
except KeyError:
|
||||||
raise ConnectorException("Invalid book data")
|
raise ConnectorException("Invalid book data")
|
||||||
# flatten the data so that images, uri, and claims are on the same level
|
# flatten the data so that images, uri, and claims are on the same level
|
||||||
return {
|
return {
|
||||||
|
@ -77,16 +75,24 @@ class Connector(AbstractConnector):
|
||||||
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]},
|
**{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]},
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_search_data(self, data, min_confidence):
|
def search(self, query, min_confidence=None): # pylint: disable=arguments-differ
|
||||||
for search_result in data.get("results", []):
|
"""overrides default search function with confidence ranking"""
|
||||||
|
results = super().search(query)
|
||||||
|
if min_confidence:
|
||||||
|
# filter the search results after the fact
|
||||||
|
return [r for r in results if r.confidence >= min_confidence]
|
||||||
|
return results
|
||||||
|
|
||||||
|
def parse_search_data(self, data):
|
||||||
|
return data.get("results")
|
||||||
|
|
||||||
|
def format_search_result(self, search_result):
|
||||||
images = search_result.get("image")
|
images = search_result.get("image")
|
||||||
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None
|
||||||
# a deeply messy translation of inventaire's scores
|
# a deeply messy translation of inventaire's scores
|
||||||
confidence = float(search_result.get("_score", 0.1))
|
confidence = float(search_result.get("_score", 0.1))
|
||||||
confidence = 0.1 if confidence < 150 else 0.999
|
confidence = 0.1 if confidence < 150 else 0.999
|
||||||
if confidence < min_confidence:
|
return SearchResult(
|
||||||
continue
|
|
||||||
yield SearchResult(
|
|
||||||
title=search_result.get("label"),
|
title=search_result.get("label"),
|
||||||
key=self.get_remote_id(search_result.get("uri")),
|
key=self.get_remote_id(search_result.get("uri")),
|
||||||
author=search_result.get("description"),
|
author=search_result.get("description"),
|
||||||
|
@ -100,12 +106,15 @@ class Connector(AbstractConnector):
|
||||||
"""got some daaaata"""
|
"""got some daaaata"""
|
||||||
results = data.get("entities")
|
results = data.get("entities")
|
||||||
if not results:
|
if not results:
|
||||||
return
|
return []
|
||||||
for search_result in list(results.values()):
|
return list(results.values())
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
|
"""totally different format than a regular search result"""
|
||||||
title = search_result.get("claims", {}).get("wdt:P1476", [])
|
title = search_result.get("claims", {}).get("wdt:P1476", [])
|
||||||
if not title:
|
if not title:
|
||||||
continue
|
return None
|
||||||
yield SearchResult(
|
return SearchResult(
|
||||||
title=title[0],
|
title=title[0],
|
||||||
key=self.get_remote_id(search_result.get("uri")),
|
key=self.get_remote_id(search_result.get("uri")),
|
||||||
author=search_result.get("description"),
|
author=search_result.get("description"),
|
||||||
|
@ -119,7 +128,6 @@ class Connector(AbstractConnector):
|
||||||
|
|
||||||
def load_edition_data(self, work_uri):
|
def load_edition_data(self, work_uri):
|
||||||
"""get a list of editions for a work"""
|
"""get a list of editions for a work"""
|
||||||
# pylint: disable=line-too-long
|
|
||||||
url = f"{self.books_url}?action=reverse-claims&property=wdt:P629&value={work_uri}&sort=true"
|
url = f"{self.books_url}?action=reverse-claims&property=wdt:P629&value={work_uri}&sort=true"
|
||||||
return get_data(url)
|
return get_data(url)
|
||||||
|
|
||||||
|
@ -201,11 +209,6 @@ class Connector(AbstractConnector):
|
||||||
return ""
|
return ""
|
||||||
return data.get("extract")
|
return data.get("extract")
|
||||||
|
|
||||||
def get_remote_id_from_model(self, obj):
|
|
||||||
"""use get_remote_id to figure out the link from a model obj"""
|
|
||||||
remote_id_value = obj.inventaire_id
|
|
||||||
return self.get_remote_id(remote_id_value)
|
|
||||||
|
|
||||||
|
|
||||||
def get_language_code(options, code="en"):
|
def get_language_code(options, code="en"):
|
||||||
"""when there are a bunch of translation but we need a single field"""
|
"""when there are a bunch of translation but we need a single field"""
|
||||||
|
|
|
@ -12,8 +12,6 @@ from .openlibrary_languages import languages
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
"""instantiate a connector for OL"""
|
"""instantiate a connector for OL"""
|
||||||
|
|
||||||
generated_remote_link_field = "openlibrary_link"
|
|
||||||
|
|
||||||
def __init__(self, identifier):
|
def __init__(self, identifier):
|
||||||
super().__init__(identifier)
|
super().__init__(identifier)
|
||||||
|
|
||||||
|
@ -68,30 +66,6 @@ class Connector(AbstractConnector):
|
||||||
Mapping("born", remote_field="birth_date"),
|
Mapping("born", remote_field="birth_date"),
|
||||||
Mapping("died", remote_field="death_date"),
|
Mapping("died", remote_field="death_date"),
|
||||||
Mapping("bio", formatter=get_description),
|
Mapping("bio", formatter=get_description),
|
||||||
Mapping(
|
|
||||||
"isni",
|
|
||||||
remote_field="remote_ids",
|
|
||||||
formatter=lambda b: get_dict_field(b, "isni"),
|
|
||||||
),
|
|
||||||
Mapping(
|
|
||||||
"asin",
|
|
||||||
remote_field="remote_ids",
|
|
||||||
formatter=lambda b: get_dict_field(b, "amazon"),
|
|
||||||
),
|
|
||||||
Mapping(
|
|
||||||
"viaf",
|
|
||||||
remote_field="remote_ids",
|
|
||||||
formatter=lambda b: get_dict_field(b, "viaf"),
|
|
||||||
),
|
|
||||||
Mapping(
|
|
||||||
"wikidata",
|
|
||||||
remote_field="remote_ids",
|
|
||||||
formatter=lambda b: get_dict_field(b, "wikidata"),
|
|
||||||
),
|
|
||||||
Mapping(
|
|
||||||
"wikipedia_link", remote_field="links", formatter=get_wikipedia_link
|
|
||||||
),
|
|
||||||
Mapping("inventaire_id", remote_field="links", formatter=get_inventaire_id),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_book_data(self, remote_id):
|
def get_book_data(self, remote_id):
|
||||||
|
@ -152,35 +126,33 @@ class Connector(AbstractConnector):
|
||||||
image_name = f"{cover_id}-{size}.jpg"
|
image_name = f"{cover_id}-{size}.jpg"
|
||||||
return f"{self.covers_url}/b/id/{image_name}"
|
return f"{self.covers_url}/b/id/{image_name}"
|
||||||
|
|
||||||
def parse_search_data(self, data, min_confidence):
|
def parse_search_data(self, data):
|
||||||
for idx, search_result in enumerate(data.get("docs")):
|
return data.get("docs")
|
||||||
|
|
||||||
|
def format_search_result(self, search_result):
|
||||||
# build the remote id from the openlibrary key
|
# build the remote id from the openlibrary key
|
||||||
key = self.books_url + search_result["key"]
|
key = self.books_url + search_result["key"]
|
||||||
author = search_result.get("author_name") or ["Unknown"]
|
author = search_result.get("author_name") or ["Unknown"]
|
||||||
cover_blob = search_result.get("cover_i")
|
cover_blob = search_result.get("cover_i")
|
||||||
cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
|
cover = self.get_cover_url([cover_blob], size="M") if cover_blob else None
|
||||||
|
return SearchResult(
|
||||||
# OL doesn't provide confidence, but it does sort by an internal ranking, so
|
|
||||||
# this confidence value is relative to the list position
|
|
||||||
confidence = 1 / (idx + 1)
|
|
||||||
|
|
||||||
yield SearchResult(
|
|
||||||
title=search_result.get("title"),
|
title=search_result.get("title"),
|
||||||
key=key,
|
key=key,
|
||||||
author=", ".join(author),
|
author=", ".join(author),
|
||||||
connector=self,
|
connector=self,
|
||||||
year=search_result.get("first_publish_year"),
|
year=search_result.get("first_publish_year"),
|
||||||
cover=cover,
|
cover=cover,
|
||||||
confidence=confidence,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_isbn_search_data(self, data):
|
def parse_isbn_search_data(self, data):
|
||||||
for search_result in list(data.values()):
|
return list(data.values())
|
||||||
|
|
||||||
|
def format_isbn_search_result(self, search_result):
|
||||||
# build the remote id from the openlibrary key
|
# build the remote id from the openlibrary key
|
||||||
key = self.books_url + search_result["key"]
|
key = self.books_url + search_result["key"]
|
||||||
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
authors = search_result.get("authors") or [{"name": "Unknown"}]
|
||||||
author_names = [author.get("name") for author in authors]
|
author_names = [author.get("name") for author in authors]
|
||||||
yield SearchResult(
|
return SearchResult(
|
||||||
title=search_result.get("title"),
|
title=search_result.get("title"),
|
||||||
key=key,
|
key=key,
|
||||||
author=", ".join(author_names),
|
author=", ".join(author_names),
|
||||||
|
@ -252,40 +224,6 @@ def get_languages(language_blob):
|
||||||
return langs
|
return langs
|
||||||
|
|
||||||
|
|
||||||
def get_dict_field(blob, field_name):
|
|
||||||
"""extract the isni from the remote id data for the author"""
|
|
||||||
if not blob or not isinstance(blob, dict):
|
|
||||||
return None
|
|
||||||
return blob.get(field_name)
|
|
||||||
|
|
||||||
|
|
||||||
def get_wikipedia_link(links):
|
|
||||||
"""extract wikipedia links"""
|
|
||||||
if not isinstance(links, list):
|
|
||||||
return None
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
if not isinstance(link, dict):
|
|
||||||
continue
|
|
||||||
if link.get("title") == "wikipedia":
|
|
||||||
return link.get("url")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_inventaire_id(links):
|
|
||||||
"""extract and format inventaire ids"""
|
|
||||||
if not isinstance(links, list):
|
|
||||||
return None
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
if not isinstance(link, dict):
|
|
||||||
continue
|
|
||||||
if link.get("title") == "inventaire.io":
|
|
||||||
iv_link = link.get("url")
|
|
||||||
return iv_link.split("/")[-1]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def pick_default_edition(options):
|
def pick_default_edition(options):
|
||||||
"""favor physical copies with covers in english"""
|
"""favor physical copies with covers in english"""
|
||||||
if not options:
|
if not options:
|
||||||
|
|
|
@ -8,20 +8,8 @@ def site_settings(request): # pylint: disable=unused-argument
|
||||||
if not request.is_secure():
|
if not request.is_secure():
|
||||||
request_protocol = "http://"
|
request_protocol = "http://"
|
||||||
|
|
||||||
site = models.SiteSettings.objects.get()
|
|
||||||
theme = "css/themes/bookwyrm-light.scss"
|
|
||||||
if (
|
|
||||||
hasattr(request, "user")
|
|
||||||
and request.user.is_authenticated
|
|
||||||
and request.user.theme
|
|
||||||
):
|
|
||||||
theme = request.user.theme.path
|
|
||||||
elif site.default_theme:
|
|
||||||
theme = site.default_theme.path
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"site": site,
|
"site": models.SiteSettings.objects.get(),
|
||||||
"site_theme": theme,
|
|
||||||
"active_announcements": models.Announcement.active_announcements(),
|
"active_announcements": models.Announcement.active_announcements(),
|
||||||
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
|
"thumbnail_generation_enabled": settings.ENABLE_THUMBNAIL_GENERATION,
|
||||||
"media_full_url": settings.MEDIA_FULL_URL,
|
"media_full_url": settings.MEDIA_FULL_URL,
|
||||||
|
|
|
@ -10,9 +10,14 @@ from bookwyrm.settings import DOMAIN
|
||||||
def email_data():
|
def email_data():
|
||||||
"""fields every email needs"""
|
"""fields every email needs"""
|
||||||
site = models.SiteSettings.objects.get()
|
site = models.SiteSettings.objects.get()
|
||||||
|
if site.logo_small:
|
||||||
|
logo_path = f"/images/{site.logo_small.url}"
|
||||||
|
else:
|
||||||
|
logo_path = "/static/images/logo-small.png"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"site_name": site.name,
|
"site_name": site.name,
|
||||||
"logo": site.logo_small_url,
|
"logo": logo_path,
|
||||||
"domain": DOMAIN,
|
"domain": DOMAIN,
|
||||||
"user": None,
|
"user": None,
|
||||||
}
|
}
|
||||||
|
@ -41,20 +46,6 @@ def password_reset_email(reset_code):
|
||||||
send_email.delay(reset_code.user.email, *format_email("password_reset", data))
|
send_email.delay(reset_code.user.email, *format_email("password_reset", data))
|
||||||
|
|
||||||
|
|
||||||
def moderation_report_email(report):
|
|
||||||
"""a report was created"""
|
|
||||||
data = email_data()
|
|
||||||
data["reporter"] = report.reporter.localname or report.reporter.username
|
|
||||||
data["reportee"] = report.user.localname or report.user.username
|
|
||||||
data["report_link"] = report.remote_id
|
|
||||||
|
|
||||||
for admin in models.User.objects.filter(
|
|
||||||
groups__name__in=["admin", "moderator"]
|
|
||||||
).distinct():
|
|
||||||
data["user"] = admin.display_name
|
|
||||||
send_email.delay(admin.email, *format_email("moderation_report", data))
|
|
||||||
|
|
||||||
|
|
||||||
def format_email(email_name, data):
|
def format_email(email_name, data):
|
||||||
"""render the email templates"""
|
"""render the email templates"""
|
||||||
subject = get_template(f"email/{email_name}/subject.html").render(data).strip()
|
subject = get_template(f"email/{email_name}/subject.html").render(data).strip()
|
||||||
|
@ -71,7 +62,7 @@ def format_email(email_name, data):
|
||||||
def send_email(recipient, subject, html_content, text_content):
|
def send_email(recipient, subject, html_content, text_content):
|
||||||
"""use a task to send the email"""
|
"""use a task to send the email"""
|
||||||
email = EmailMultiAlternatives(
|
email = EmailMultiAlternatives(
|
||||||
subject, text_content, settings.EMAIL_SENDER, [recipient]
|
subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient]
|
||||||
)
|
)
|
||||||
email.attach_alternative(html_content, "text/html")
|
email.attach_alternative(html_content, "text/html")
|
||||||
email.send()
|
email.send()
|
||||||
|
|
337
bookwyrm/forms.py
Normal file
337
bookwyrm/forms.py
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
""" using django model forms """
|
||||||
|
import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
|
||||||
|
from django.forms.widgets import Textarea
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
||||||
|
class CustomForm(ModelForm):
|
||||||
|
"""add css classes to the forms"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
css_classes = defaultdict(lambda: "")
|
||||||
|
css_classes["text"] = "input"
|
||||||
|
css_classes["password"] = "input"
|
||||||
|
css_classes["email"] = "input"
|
||||||
|
css_classes["number"] = "input"
|
||||||
|
css_classes["checkbox"] = "checkbox"
|
||||||
|
css_classes["textarea"] = "textarea"
|
||||||
|
# pylint: disable=super-with-arguments
|
||||||
|
super(CustomForm, self).__init__(*args, **kwargs)
|
||||||
|
for visible in self.visible_fields():
|
||||||
|
if hasattr(visible.field.widget, "input_type"):
|
||||||
|
input_type = visible.field.widget.input_type
|
||||||
|
if isinstance(visible.field.widget, Textarea):
|
||||||
|
input_type = "textarea"
|
||||||
|
visible.field.widget.attrs["rows"] = 5
|
||||||
|
visible.field.widget.attrs["class"] = css_classes[input_type]
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=missing-class-docstring
|
||||||
|
class LoginForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["localname", "password"]
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
widgets = {
|
||||||
|
"password": PasswordInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["localname", "email", "password"]
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
widgets = {"password": PasswordInput()}
|
||||||
|
|
||||||
|
|
||||||
|
class RatingForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.ReviewRating
|
||||||
|
fields = ["user", "book", "rating", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Review
|
||||||
|
fields = [
|
||||||
|
"user",
|
||||||
|
"book",
|
||||||
|
"name",
|
||||||
|
"content",
|
||||||
|
"rating",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"privacy",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Comment
|
||||||
|
fields = [
|
||||||
|
"user",
|
||||||
|
"book",
|
||||||
|
"content",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"privacy",
|
||||||
|
"progress",
|
||||||
|
"progress_mode",
|
||||||
|
"reading_status",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class QuotationForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Quotation
|
||||||
|
fields = [
|
||||||
|
"user",
|
||||||
|
"book",
|
||||||
|
"quote",
|
||||||
|
"content",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"privacy",
|
||||||
|
"position",
|
||||||
|
"position_mode",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ReplyForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Status
|
||||||
|
fields = [
|
||||||
|
"user",
|
||||||
|
"content",
|
||||||
|
"content_warning",
|
||||||
|
"sensitive",
|
||||||
|
"reply_parent",
|
||||||
|
"privacy",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StatusForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Status
|
||||||
|
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class DirectForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Status
|
||||||
|
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class EditUserForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = [
|
||||||
|
"avatar",
|
||||||
|
"name",
|
||||||
|
"email",
|
||||||
|
"summary",
|
||||||
|
"show_goal",
|
||||||
|
"show_suggested_users",
|
||||||
|
"manually_approves_followers",
|
||||||
|
"default_post_privacy",
|
||||||
|
"discoverable",
|
||||||
|
"preferred_timezone",
|
||||||
|
"preferred_language",
|
||||||
|
]
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
|
class LimitedEditUserForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = [
|
||||||
|
"avatar",
|
||||||
|
"name",
|
||||||
|
"summary",
|
||||||
|
"manually_approves_followers",
|
||||||
|
"discoverable",
|
||||||
|
]
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteUserForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["password"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["groups"]
|
||||||
|
|
||||||
|
|
||||||
|
class CoverForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Book
|
||||||
|
fields = ["cover"]
|
||||||
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
|
class EditionForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Edition
|
||||||
|
exclude = [
|
||||||
|
"remote_id",
|
||||||
|
"origin_id",
|
||||||
|
"created_date",
|
||||||
|
"updated_date",
|
||||||
|
"edition_rank",
|
||||||
|
"authors",
|
||||||
|
"parent_work",
|
||||||
|
"shelves",
|
||||||
|
"connector",
|
||||||
|
"search_vector",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Author
|
||||||
|
exclude = [
|
||||||
|
"remote_id",
|
||||||
|
"origin_id",
|
||||||
|
"created_date",
|
||||||
|
"updated_date",
|
||||||
|
"search_vector",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ImportForm(forms.Form):
|
||||||
|
csv_file = forms.FileField()
|
||||||
|
|
||||||
|
|
||||||
|
class ExpiryWidget(widgets.Select):
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
"""human-readable exiration time buckets"""
|
||||||
|
selected_string = super().value_from_datadict(data, files, name)
|
||||||
|
|
||||||
|
if selected_string == "day":
|
||||||
|
interval = datetime.timedelta(days=1)
|
||||||
|
elif selected_string == "week":
|
||||||
|
interval = datetime.timedelta(days=7)
|
||||||
|
elif selected_string == "month":
|
||||||
|
interval = datetime.timedelta(days=31) # Close enough?
|
||||||
|
elif selected_string == "forever":
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return selected_string # This will raise
|
||||||
|
|
||||||
|
return timezone.now() + interval
|
||||||
|
|
||||||
|
|
||||||
|
class InviteRequestForm(CustomForm):
|
||||||
|
def clean(self):
|
||||||
|
"""make sure the email isn't in use by a registered user"""
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
email = cleaned_data.get("email")
|
||||||
|
if email and models.User.objects.filter(email=email).exists():
|
||||||
|
self.add_error("email", _("A user with this email already exists."))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.InviteRequest
|
||||||
|
fields = ["email"]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateInviteForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.SiteInvite
|
||||||
|
exclude = ["code", "user", "times_used", "invitees"]
|
||||||
|
widgets = {
|
||||||
|
"expiry": ExpiryWidget(
|
||||||
|
choices=[
|
||||||
|
("day", _("One Day")),
|
||||||
|
("week", _("One Week")),
|
||||||
|
("month", _("One Month")),
|
||||||
|
("forever", _("Does Not Expire")),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"use_limit": widgets.Select(
|
||||||
|
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
|
||||||
|
+ [(None, _("Unlimited"))]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ShelfForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Shelf
|
||||||
|
fields = ["user", "name", "privacy", "description"]
|
||||||
|
|
||||||
|
|
||||||
|
class GoalForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.AnnualGoal
|
||||||
|
fields = ["user", "year", "goal", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class SiteForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.SiteSettings
|
||||||
|
exclude = []
|
||||||
|
|
||||||
|
|
||||||
|
class AnnouncementForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Announcement
|
||||||
|
exclude = ["remote_id"]
|
||||||
|
|
||||||
|
|
||||||
|
class ListForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.List
|
||||||
|
fields = ["user", "name", "description", "curation", "privacy"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReportForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Report
|
||||||
|
fields = ["user", "reporter", "statuses", "note"]
|
||||||
|
|
||||||
|
|
||||||
|
class EmailBlocklistForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.EmailBlocklist
|
||||||
|
fields = ["domain"]
|
||||||
|
|
||||||
|
|
||||||
|
class IPBlocklistForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.IPBlocklist
|
||||||
|
fields = ["address"]
|
||||||
|
|
||||||
|
|
||||||
|
class ServerForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.FederatedServer
|
||||||
|
exclude = ["remote_id"]
|
||||||
|
|
||||||
|
|
||||||
|
class SortListForm(forms.Form):
|
||||||
|
sort_by = ChoiceField(
|
||||||
|
choices=(
|
||||||
|
("order", _("List Order")),
|
||||||
|
("title", _("Book Title")),
|
||||||
|
("rating", _("Rating")),
|
||||||
|
),
|
||||||
|
label=_("Sort By"),
|
||||||
|
)
|
||||||
|
direction = ChoiceField(
|
||||||
|
choices=(
|
||||||
|
("ascending", _("Ascending")),
|
||||||
|
("descending", _("Descending")),
|
||||||
|
),
|
||||||
|
)
|
|
@ -1,12 +0,0 @@
|
||||||
""" make forms available to the app """
|
|
||||||
# site admin
|
|
||||||
from .admin import *
|
|
||||||
from .author import *
|
|
||||||
from .books import *
|
|
||||||
from .edit_user import *
|
|
||||||
from .forms import *
|
|
||||||
from .groups import *
|
|
||||||
from .landing import *
|
|
||||||
from .links import *
|
|
||||||
from .lists import *
|
|
||||||
from .status import *
|
|
|
@ -1,141 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.forms import widgets
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django_celery_beat.models import IntervalSchedule
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class ExpiryWidget(widgets.Select):
|
|
||||||
def value_from_datadict(self, data, files, name):
|
|
||||||
"""human-readable exiration time buckets"""
|
|
||||||
selected_string = super().value_from_datadict(data, files, name)
|
|
||||||
|
|
||||||
if selected_string == "day":
|
|
||||||
interval = datetime.timedelta(days=1)
|
|
||||||
elif selected_string == "week":
|
|
||||||
interval = datetime.timedelta(days=7)
|
|
||||||
elif selected_string == "month":
|
|
||||||
interval = datetime.timedelta(days=31) # Close enough?
|
|
||||||
elif selected_string == "forever":
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return selected_string # This will raise
|
|
||||||
|
|
||||||
return timezone.now() + interval
|
|
||||||
|
|
||||||
|
|
||||||
class CreateInviteForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.SiteInvite
|
|
||||||
exclude = ["code", "user", "times_used", "invitees"]
|
|
||||||
widgets = {
|
|
||||||
"expiry": ExpiryWidget(
|
|
||||||
choices=[
|
|
||||||
("day", _("One Day")),
|
|
||||||
("week", _("One Week")),
|
|
||||||
("month", _("One Month")),
|
|
||||||
("forever", _("Does Not Expire")),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"use_limit": widgets.Select(
|
|
||||||
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
|
|
||||||
+ [(None, _("Unlimited"))]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SiteForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.SiteSettings
|
|
||||||
exclude = ["admin_code", "install_mode"]
|
|
||||||
widgets = {
|
|
||||||
"instance_short_description": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_instance_short_description"}
|
|
||||||
),
|
|
||||||
"require_confirm_email": forms.CheckboxInput(
|
|
||||||
attrs={"aria-describedby": "desc_require_confirm_email"}
|
|
||||||
),
|
|
||||||
"invite_request_text": forms.Textarea(
|
|
||||||
attrs={"aria-describedby": "desc_invite_request_text"}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ThemeForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Theme
|
|
||||||
fields = ["name", "path"]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
|
||||||
"path": forms.TextInput(
|
|
||||||
attrs={
|
|
||||||
"aria-describedby": "desc_path",
|
|
||||||
"placeholder": "css/themes/theme-name.scss",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AnnouncementForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Announcement
|
|
||||||
exclude = ["remote_id"]
|
|
||||||
widgets = {
|
|
||||||
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
|
|
||||||
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
|
|
||||||
"event_date": forms.SelectDateWidget(
|
|
||||||
attrs={"aria-describedby": "desc_event_date"}
|
|
||||||
),
|
|
||||||
"start_date": forms.SelectDateWidget(
|
|
||||||
attrs={"aria-describedby": "desc_start_date"}
|
|
||||||
),
|
|
||||||
"end_date": forms.SelectDateWidget(
|
|
||||||
attrs={"aria-describedby": "desc_end_date"}
|
|
||||||
),
|
|
||||||
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EmailBlocklistForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.EmailBlocklist
|
|
||||||
fields = ["domain"]
|
|
||||||
widgets = {
|
|
||||||
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class IPBlocklistForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.IPBlocklist
|
|
||||||
fields = ["address"]
|
|
||||||
|
|
||||||
|
|
||||||
class ServerForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.FederatedServer
|
|
||||||
exclude = ["remote_id"]
|
|
||||||
|
|
||||||
|
|
||||||
class AutoModRuleForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.AutoMod
|
|
||||||
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
|
|
||||||
|
|
||||||
|
|
||||||
class IntervalScheduleForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = IntervalSchedule
|
|
||||||
fields = ["every", "period"]
|
|
||||||
|
|
||||||
widgets = {
|
|
||||||
"every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}),
|
|
||||||
"period": forms.Select(attrs={"aria-describedby": "desc_period"}),
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class AuthorForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Author
|
|
||||||
fields = [
|
|
||||||
"last_edited_by",
|
|
||||||
"name",
|
|
||||||
"aliases",
|
|
||||||
"bio",
|
|
||||||
"wikipedia_link",
|
|
||||||
"born",
|
|
||||||
"died",
|
|
||||||
"openlibrary_key",
|
|
||||||
"inventaire_id",
|
|
||||||
"librarything_key",
|
|
||||||
"goodreads_key",
|
|
||||||
"isni",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
|
||||||
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
|
|
||||||
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
|
|
||||||
"wikipedia_link": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_wikipedia_link"}
|
|
||||||
),
|
|
||||||
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
|
|
||||||
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
|
|
||||||
"oepnlibrary_key": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_oepnlibrary_key"}
|
|
||||||
),
|
|
||||||
"inventaire_id": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
|
||||||
),
|
|
||||||
"librarything_key": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_librarything_key"}
|
|
||||||
),
|
|
||||||
"goodreads_key": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_goodreads_key"}
|
|
||||||
),
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
from .widgets import ArrayWidget, SelectDateWidget, Select
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class CoverForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Book
|
|
||||||
fields = ["cover"]
|
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
|
|
||||||
|
|
||||||
class EditionForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Edition
|
|
||||||
exclude = [
|
|
||||||
"remote_id",
|
|
||||||
"origin_id",
|
|
||||||
"created_date",
|
|
||||||
"updated_date",
|
|
||||||
"edition_rank",
|
|
||||||
"authors",
|
|
||||||
"parent_work",
|
|
||||||
"shelves",
|
|
||||||
"connector",
|
|
||||||
"search_vector",
|
|
||||||
"links",
|
|
||||||
"file_links",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
|
|
||||||
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
|
|
||||||
"description": forms.Textarea(
|
|
||||||
attrs={"aria-describedby": "desc_description"}
|
|
||||||
),
|
|
||||||
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
|
|
||||||
"series_number": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_series_number"}
|
|
||||||
),
|
|
||||||
"subjects": ArrayWidget(),
|
|
||||||
"languages": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_languages_help desc_languages"}
|
|
||||||
),
|
|
||||||
"publishers": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
|
|
||||||
),
|
|
||||||
"first_published_date": SelectDateWidget(
|
|
||||||
attrs={"aria-describedby": "desc_first_published_date"}
|
|
||||||
),
|
|
||||||
"published_date": SelectDateWidget(
|
|
||||||
attrs={"aria-describedby": "desc_published_date"}
|
|
||||||
),
|
|
||||||
"cover": ClearableFileInputWithWarning(
|
|
||||||
attrs={"aria-describedby": "desc_cover"}
|
|
||||||
),
|
|
||||||
"physical_format": Select(
|
|
||||||
attrs={"aria-describedby": "desc_physical_format"}
|
|
||||||
),
|
|
||||||
"physical_format_detail": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_physical_format_detail"}
|
|
||||||
),
|
|
||||||
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
|
|
||||||
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
|
|
||||||
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
|
|
||||||
"openlibrary_key": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_openlibrary_key"}
|
|
||||||
),
|
|
||||||
"inventaire_id": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_inventaire_id"}
|
|
||||||
),
|
|
||||||
"oclc_number": forms.TextInput(
|
|
||||||
attrs={"aria-describedby": "desc_oclc_number"}
|
|
||||||
),
|
|
||||||
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class EditionFromWorkForm(CustomForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
# make all fields hidden
|
|
||||||
for visible in self.visible_fields():
|
|
||||||
visible.field.widget = forms.HiddenInput()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.Work
|
|
||||||
fields = [
|
|
||||||
"title",
|
|
||||||
"subtitle",
|
|
||||||
"authors",
|
|
||||||
"description",
|
|
||||||
"languages",
|
|
||||||
"series",
|
|
||||||
"series_number",
|
|
||||||
"subjects",
|
|
||||||
"subject_places",
|
|
||||||
"cover",
|
|
||||||
"first_published_date",
|
|
||||||
]
|
|
|
@ -1,26 +0,0 @@
|
||||||
""" Overrides django's default form class """
|
|
||||||
from collections import defaultdict
|
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.forms.widgets import Textarea
|
|
||||||
|
|
||||||
|
|
||||||
class CustomForm(ModelForm):
|
|
||||||
"""add css classes to the forms"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
css_classes = defaultdict(lambda: "")
|
|
||||||
css_classes["text"] = "input"
|
|
||||||
css_classes["password"] = "input"
|
|
||||||
css_classes["email"] = "input"
|
|
||||||
css_classes["number"] = "input"
|
|
||||||
css_classes["checkbox"] = "checkbox"
|
|
||||||
css_classes["textarea"] = "textarea"
|
|
||||||
# pylint: disable=super-with-arguments
|
|
||||||
super(CustomForm, self).__init__(*args, **kwargs)
|
|
||||||
for visible in self.visible_fields():
|
|
||||||
if hasattr(visible.field.widget, "input_type"):
|
|
||||||
input_type = visible.field.widget.input_type
|
|
||||||
if isinstance(visible.field.widget, Textarea):
|
|
||||||
input_type = "textarea"
|
|
||||||
visible.field.widget.attrs["rows"] = 5
|
|
||||||
visible.field.widget.attrs["class"] = css_classes[input_type]
|
|
|
@ -1,68 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.models.fields import ClearableFileInputWithWarning
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class EditUserForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = [
|
|
||||||
"avatar",
|
|
||||||
"name",
|
|
||||||
"email",
|
|
||||||
"summary",
|
|
||||||
"show_goal",
|
|
||||||
"show_suggested_users",
|
|
||||||
"manually_approves_followers",
|
|
||||||
"default_post_privacy",
|
|
||||||
"discoverable",
|
|
||||||
"hide_follows",
|
|
||||||
"preferred_timezone",
|
|
||||||
"preferred_language",
|
|
||||||
"theme",
|
|
||||||
]
|
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
widgets = {
|
|
||||||
"avatar": ClearableFileInputWithWarning(
|
|
||||||
attrs={"aria-describedby": "desc_avatar"}
|
|
||||||
),
|
|
||||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
|
||||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
|
||||||
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
|
|
||||||
"discoverable": forms.CheckboxInput(
|
|
||||||
attrs={"aria-describedby": "desc_discoverable"}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedEditUserForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = [
|
|
||||||
"avatar",
|
|
||||||
"name",
|
|
||||||
"summary",
|
|
||||||
"manually_approves_followers",
|
|
||||||
"discoverable",
|
|
||||||
]
|
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
widgets = {
|
|
||||||
"avatar": ClearableFileInputWithWarning(
|
|
||||||
attrs={"aria-describedby": "desc_avatar"}
|
|
||||||
),
|
|
||||||
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
|
|
||||||
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
|
|
||||||
"discoverable": forms.CheckboxInput(
|
|
||||||
attrs={"aria-describedby": "desc_discoverable"}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteUserForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = ["password"]
|
|
|
@ -1,64 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from django import forms
|
|
||||||
from django.forms import widgets
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.models.user import FeedFilterChoices
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class FeedStatusTypesForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = ["feed_status_types"]
|
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
widgets = {
|
|
||||||
"feed_status_types": widgets.CheckboxSelectMultiple(
|
|
||||||
choices=FeedFilterChoices,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(forms.Form):
|
|
||||||
csv_file = forms.FileField()
|
|
||||||
|
|
||||||
|
|
||||||
class ShelfForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Shelf
|
|
||||||
fields = ["user", "name", "privacy", "description"]
|
|
||||||
|
|
||||||
|
|
||||||
class GoalForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.AnnualGoal
|
|
||||||
fields = ["user", "year", "goal", "privacy"]
|
|
||||||
|
|
||||||
|
|
||||||
class ReportForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Report
|
|
||||||
fields = ["user", "reporter", "status", "links", "note"]
|
|
||||||
|
|
||||||
|
|
||||||
class ReadThroughForm(CustomForm):
|
|
||||||
def clean(self):
|
|
||||||
"""don't let readthroughs end before they start"""
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
start_date = cleaned_data.get("start_date")
|
|
||||||
finish_date = cleaned_data.get("finish_date")
|
|
||||||
if start_date and finish_date and start_date > finish_date:
|
|
||||||
self.add_error(
|
|
||||||
"finish_date", _("Reading finish date cannot be before start date.")
|
|
||||||
)
|
|
||||||
stopped_date = cleaned_data.get("stopped_date")
|
|
||||||
if start_date and stopped_date and start_date > stopped_date:
|
|
||||||
self.add_error(
|
|
||||||
"stopped_date", _("Reading stopped date cannot be before start date.")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.ReadThrough
|
|
||||||
fields = ["user", "book", "start_date", "finish_date", "stopped_date"]
|
|
|
@ -1,16 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class UserGroupForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = ["groups"]
|
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Group
|
|
||||||
fields = ["user", "privacy", "name", "description"]
|
|
|
@ -1,45 +0,0 @@
|
||||||
""" Forms for the landing pages """
|
|
||||||
from django.forms import PasswordInput
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class LoginForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = ["localname", "password"]
|
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
widgets = {
|
|
||||||
"password": PasswordInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.User
|
|
||||||
fields = ["localname", "email", "password"]
|
|
||||||
help_texts = {f: None for f in fields}
|
|
||||||
widgets = {"password": PasswordInput()}
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""Check if the username is taken"""
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
localname = cleaned_data.get("localname").strip()
|
|
||||||
if models.User.objects.filter(localname=localname).first():
|
|
||||||
self.add_error("localname", _("User with this username already exists"))
|
|
||||||
|
|
||||||
|
|
||||||
class InviteRequestForm(CustomForm):
|
|
||||||
def clean(self):
|
|
||||||
"""make sure the email isn't in use by a registered user"""
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
email = cleaned_data.get("email")
|
|
||||||
if email and models.User.objects.filter(email=email).exists():
|
|
||||||
self.add_error("email", _("A user with this email already exists."))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.InviteRequest
|
|
||||||
fields = ["email", "answer"]
|
|
|
@ -1,48 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class LinkDomainForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.LinkDomain
|
|
||||||
fields = ["name"]
|
|
||||||
|
|
||||||
|
|
||||||
class FileLinkForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.FileLink
|
|
||||||
fields = ["url", "filetype", "availability", "book", "added_by"]
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
"""make sure the domain isn't blocked or pending"""
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
url = cleaned_data.get("url")
|
|
||||||
filetype = cleaned_data.get("filetype")
|
|
||||||
book = cleaned_data.get("book")
|
|
||||||
domain = urlparse(url).netloc
|
|
||||||
if models.LinkDomain.objects.filter(domain=domain).exists():
|
|
||||||
status = models.LinkDomain.objects.get(domain=domain).status
|
|
||||||
if status == "blocked":
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
self.add_error(
|
|
||||||
"url",
|
|
||||||
_(
|
|
||||||
"This domain is blocked. Please contact your administrator if you think this is an error."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
elif models.FileLink.objects.filter(
|
|
||||||
url=url, book=book, filetype=filetype
|
|
||||||
).exists():
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
self.add_error(
|
|
||||||
"url",
|
|
||||||
_(
|
|
||||||
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,37 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from django import forms
|
|
||||||
from django.forms import ChoiceField
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class ListForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.List
|
|
||||||
fields = ["user", "name", "description", "curation", "privacy", "group"]
|
|
||||||
|
|
||||||
|
|
||||||
class ListItemForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.ListItem
|
|
||||||
fields = ["user", "book", "book_list", "notes"]
|
|
||||||
|
|
||||||
|
|
||||||
class SortListForm(forms.Form):
|
|
||||||
sort_by = ChoiceField(
|
|
||||||
choices=(
|
|
||||||
("order", _("List Order")),
|
|
||||||
("title", _("Book Title")),
|
|
||||||
("rating", _("Rating")),
|
|
||||||
),
|
|
||||||
label=_("Sort By"),
|
|
||||||
)
|
|
||||||
direction = ChoiceField(
|
|
||||||
choices=(
|
|
||||||
("ascending", _("Ascending")),
|
|
||||||
("descending", _("Descending")),
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,82 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from bookwyrm import models
|
|
||||||
from .custom_form import CustomForm
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=missing-class-docstring
|
|
||||||
class RatingForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.ReviewRating
|
|
||||||
fields = ["user", "book", "rating", "privacy"]
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Review
|
|
||||||
fields = [
|
|
||||||
"user",
|
|
||||||
"book",
|
|
||||||
"name",
|
|
||||||
"content",
|
|
||||||
"rating",
|
|
||||||
"content_warning",
|
|
||||||
"sensitive",
|
|
||||||
"privacy",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Comment
|
|
||||||
fields = [
|
|
||||||
"user",
|
|
||||||
"book",
|
|
||||||
"content",
|
|
||||||
"content_warning",
|
|
||||||
"sensitive",
|
|
||||||
"privacy",
|
|
||||||
"progress",
|
|
||||||
"progress_mode",
|
|
||||||
"reading_status",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class QuotationForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Quotation
|
|
||||||
fields = [
|
|
||||||
"user",
|
|
||||||
"book",
|
|
||||||
"quote",
|
|
||||||
"content",
|
|
||||||
"content_warning",
|
|
||||||
"sensitive",
|
|
||||||
"privacy",
|
|
||||||
"position",
|
|
||||||
"position_mode",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ReplyForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Status
|
|
||||||
fields = [
|
|
||||||
"user",
|
|
||||||
"content",
|
|
||||||
"content_warning",
|
|
||||||
"sensitive",
|
|
||||||
"reply_parent",
|
|
||||||
"privacy",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class StatusForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Status
|
|
||||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
|
||||||
|
|
||||||
|
|
||||||
class DirectForm(CustomForm):
|
|
||||||
class Meta:
|
|
||||||
model = models.Status
|
|
||||||
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
|
|
|
@ -1,70 +0,0 @@
|
||||||
""" using django model forms """
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayWidget(forms.widgets.TextInput):
|
|
||||||
"""Inputs for postgres array fields"""
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def value_from_datadict(self, data, files, name):
|
|
||||||
"""get all values for this name"""
|
|
||||||
return [i for i in data.getlist(name) if i]
|
|
||||||
|
|
||||||
|
|
||||||
class Select(forms.Select):
|
|
||||||
"""custom template for select widget"""
|
|
||||||
|
|
||||||
template_name = "widgets/select.html"
|
|
||||||
|
|
||||||
|
|
||||||
class SelectDateWidget(forms.SelectDateWidget):
|
|
||||||
"""
|
|
||||||
A widget that splits date input into two <select> boxes and a numerical year.
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "widgets/addon_multiwidget.html"
|
|
||||||
select_widget = Select
|
|
||||||
|
|
||||||
def get_context(self, name, value, attrs):
|
|
||||||
"""sets individual widgets"""
|
|
||||||
context = super().get_context(name, value, attrs)
|
|
||||||
date_context = {}
|
|
||||||
year_name = self.year_field % name
|
|
||||||
date_context["year"] = forms.NumberInput().get_context(
|
|
||||||
name=year_name,
|
|
||||||
value=context["widget"]["value"]["year"],
|
|
||||||
attrs={
|
|
||||||
**context["widget"]["attrs"],
|
|
||||||
"id": f"id_{year_name}",
|
|
||||||
"class": "input",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
month_choices = list(self.months.items())
|
|
||||||
if not self.is_required:
|
|
||||||
month_choices.insert(0, self.month_none_value)
|
|
||||||
month_name = self.month_field % name
|
|
||||||
date_context["month"] = self.select_widget(
|
|
||||||
attrs, choices=month_choices
|
|
||||||
).get_context(
|
|
||||||
name=month_name,
|
|
||||||
value=context["widget"]["value"]["month"],
|
|
||||||
attrs={**context["widget"]["attrs"], "id": f"id_{month_name}"},
|
|
||||||
)
|
|
||||||
day_choices = [(i, i) for i in range(1, 32)]
|
|
||||||
if not self.is_required:
|
|
||||||
day_choices.insert(0, self.day_none_value)
|
|
||||||
day_name = self.day_field % name
|
|
||||||
date_context["day"] = self.select_widget(
|
|
||||||
attrs,
|
|
||||||
choices=day_choices,
|
|
||||||
).get_context(
|
|
||||||
name=day_name,
|
|
||||||
value=context["widget"]["value"]["day"],
|
|
||||||
attrs={**context["widget"]["attrs"], "id": f"id_{day_name}"},
|
|
||||||
)
|
|
||||||
subwidgets = []
|
|
||||||
for field in self._parse_date_fmt():
|
|
||||||
subwidgets.append(date_context[field]["widget"])
|
|
||||||
context["widget"]["subwidgets"] = subwidgets
|
|
||||||
return context
|
|
|
@ -1,8 +1,6 @@
|
||||||
""" import classes """
|
""" import classes """
|
||||||
|
|
||||||
from .importer import Importer
|
from .importer import Importer
|
||||||
from .calibre_import import CalibreImporter
|
|
||||||
from .goodreads_import import GoodreadsImporter
|
from .goodreads_import import GoodreadsImporter
|
||||||
from .librarything_import import LibrarythingImporter
|
from .librarything_import import LibrarythingImporter
|
||||||
from .openlibrary_import import OpenLibraryImporter
|
|
||||||
from .storygraph_import import StorygraphImporter
|
from .storygraph_import import StorygraphImporter
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
""" handle reading a csv from calibre """
|
|
||||||
from bookwyrm.models import Shelf
|
|
||||||
|
|
||||||
from . import Importer
|
|
||||||
|
|
||||||
|
|
||||||
class CalibreImporter(Importer):
|
|
||||||
"""csv downloads from Calibre"""
|
|
||||||
|
|
||||||
service = "Calibre"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
# Add timestamp to row_mappings_guesses for date_added to avoid
|
|
||||||
# integrity error
|
|
||||||
row_mappings_guesses = []
|
|
||||||
|
|
||||||
for field, mapping in self.row_mappings_guesses:
|
|
||||||
if field in ("date_added",):
|
|
||||||
row_mappings_guesses.append((field, mapping + ["timestamp"]))
|
|
||||||
else:
|
|
||||||
row_mappings_guesses.append((field, mapping))
|
|
||||||
|
|
||||||
self.row_mappings_guesses = row_mappings_guesses
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_shelf(self, normalized_row):
|
|
||||||
# Calibre export does not indicate which shelf to use. Go with a default one for now
|
|
||||||
return Shelf.TO_READ
|
|
|
@ -3,7 +3,14 @@ from . import Importer
|
||||||
|
|
||||||
|
|
||||||
class GoodreadsImporter(Importer):
|
class GoodreadsImporter(Importer):
|
||||||
"""Goodreads is the default importer, thus Importer follows its structure.
|
"""GoodReads is the default importer, thus Importer follows its structure.
|
||||||
For a more complete example of overriding see librarything_import.py"""
|
For a more complete example of overriding see librarything_import.py"""
|
||||||
|
|
||||||
service = "Goodreads"
|
service = "GoodReads"
|
||||||
|
|
||||||
|
def parse_fields(self, entry):
|
||||||
|
"""handle the specific fields in goodreads csvs"""
|
||||||
|
entry.update({"import_source": self.service})
|
||||||
|
# add missing 'Date Started' field
|
||||||
|
entry.update({"Date Started": None})
|
||||||
|
return entry
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" handle reading a csv from an external service, defaults are from Goodreads """
|
""" handle reading a csv from an external service, defaults are from GoodReads """
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
from bookwyrm.models import ImportJob, ImportItem
|
from bookwyrm.models import ImportJob, ImportItem
|
||||||
from bookwyrm.tasks import app, LOW
|
from bookwyrm.tasks import app
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -15,93 +15,33 @@ logger = logging.getLogger(__name__)
|
||||||
class Importer:
|
class Importer:
|
||||||
"""Generic class for csv data import from an outside service"""
|
"""Generic class for csv data import from an outside service"""
|
||||||
|
|
||||||
service = "Import"
|
service = "Unknown"
|
||||||
delimiter = ","
|
delimiter = ","
|
||||||
encoding = "UTF-8"
|
encoding = "UTF-8"
|
||||||
|
mandatory_fields = ["Title", "Author"]
|
||||||
# these are from Goodreads
|
|
||||||
row_mappings_guesses = [
|
|
||||||
("id", ["id", "book id"]),
|
|
||||||
("title", ["title"]),
|
|
||||||
("authors", ["author", "authors", "primary author"]),
|
|
||||||
("isbn_10", ["isbn10", "isbn"]),
|
|
||||||
("isbn_13", ["isbn13", "isbn", "isbns"]),
|
|
||||||
("shelf", ["shelf", "exclusive shelf", "read status", "bookshelf"]),
|
|
||||||
("review_name", ["review name"]),
|
|
||||||
("review_body", ["my review", "review"]),
|
|
||||||
("rating", ["my rating", "rating", "star rating"]),
|
|
||||||
("date_added", ["date added", "entry date", "added"]),
|
|
||||||
("date_started", ["date started", "started"]),
|
|
||||||
("date_finished", ["date finished", "last date read", "date read", "finished"]),
|
|
||||||
]
|
|
||||||
date_fields = ["date_added", "date_started", "date_finished"]
|
|
||||||
shelf_mapping_guesses = {
|
|
||||||
"to-read": ["to-read", "want to read"],
|
|
||||||
"read": ["read", "already read"],
|
|
||||||
"reading": ["currently-reading", "reading", "currently reading"],
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_job(self, user, csv_file, include_reviews, privacy):
|
def create_job(self, user, csv_file, include_reviews, privacy):
|
||||||
"""check over a csv and creates a database entry for the job"""
|
"""check over a csv and creates a database entry for the job"""
|
||||||
csv_reader = csv.DictReader(csv_file, delimiter=self.delimiter)
|
|
||||||
rows = enumerate(list(csv_reader))
|
|
||||||
job = ImportJob.objects.create(
|
job = ImportJob.objects.create(
|
||||||
user=user,
|
user=user, include_reviews=include_reviews, privacy=privacy
|
||||||
include_reviews=include_reviews,
|
|
||||||
privacy=privacy,
|
|
||||||
mappings=self.create_row_mappings(csv_reader.fieldnames),
|
|
||||||
source=self.service,
|
|
||||||
)
|
)
|
||||||
|
for index, entry in enumerate(
|
||||||
for index, entry in rows:
|
list(csv.DictReader(csv_file, delimiter=self.delimiter))
|
||||||
self.create_item(job, index, entry)
|
):
|
||||||
|
if not all(x in entry for x in self.mandatory_fields):
|
||||||
|
raise ValueError("Author and title must be in data.")
|
||||||
|
entry = self.parse_fields(entry)
|
||||||
|
self.save_item(job, index, entry)
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def update_legacy_job(self, job):
|
def save_item(self, job, index, data): # pylint: disable=no-self-use
|
||||||
"""patch up a job that was in the old format"""
|
|
||||||
items = job.items
|
|
||||||
headers = list(items.first().data.keys())
|
|
||||||
job.mappings = self.create_row_mappings(headers)
|
|
||||||
job.updated_date = timezone.now()
|
|
||||||
job.save()
|
|
||||||
|
|
||||||
for item in items.all():
|
|
||||||
normalized = self.normalize_row(item.data, job.mappings)
|
|
||||||
normalized["shelf"] = self.get_shelf(normalized)
|
|
||||||
item.normalized_data = normalized
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
def create_row_mappings(self, headers):
|
|
||||||
"""guess what the headers mean"""
|
|
||||||
mappings = {}
|
|
||||||
for (key, guesses) in self.row_mappings_guesses:
|
|
||||||
value = [h for h in headers if h.lower() in guesses]
|
|
||||||
value = value[0] if len(value) else None
|
|
||||||
if value:
|
|
||||||
headers.remove(value)
|
|
||||||
mappings[key] = value
|
|
||||||
return mappings
|
|
||||||
|
|
||||||
def create_item(self, job, index, data):
|
|
||||||
"""creates and saves an import item"""
|
"""creates and saves an import item"""
|
||||||
normalized = self.normalize_row(data, job.mappings)
|
ImportItem(job=job, index=index, data=data).save()
|
||||||
normalized["shelf"] = self.get_shelf(normalized)
|
|
||||||
ImportItem(job=job, index=index, data=data, normalized_data=normalized).save()
|
|
||||||
|
|
||||||
def get_shelf(self, normalized_row):
|
def parse_fields(self, entry):
|
||||||
"""determine which shelf to use"""
|
"""updates csv data with additional info"""
|
||||||
shelf_name = normalized_row.get("shelf")
|
entry.update({"import_source": self.service})
|
||||||
if not shelf_name:
|
return entry
|
||||||
return None
|
|
||||||
shelf_name = shelf_name.lower()
|
|
||||||
shelf = [
|
|
||||||
s for (s, gs) in self.shelf_mapping_guesses.items() if shelf_name in gs
|
|
||||||
]
|
|
||||||
return shelf[0] if shelf else None
|
|
||||||
|
|
||||||
def normalize_row(self, entry, mappings): # pylint: disable=no-self-use
|
|
||||||
"""use the dataclass to create the formatted row of data"""
|
|
||||||
return {k: entry.get(v) for k, v in mappings.items()}
|
|
||||||
|
|
||||||
def create_retry_job(self, user, original_job, items):
|
def create_retry_job(self, user, original_job, items):
|
||||||
"""retry items that didn't import"""
|
"""retry items that didn't import"""
|
||||||
|
@ -109,66 +49,55 @@ class Importer:
|
||||||
user=user,
|
user=user,
|
||||||
include_reviews=original_job.include_reviews,
|
include_reviews=original_job.include_reviews,
|
||||||
privacy=original_job.privacy,
|
privacy=original_job.privacy,
|
||||||
source=original_job.source,
|
|
||||||
# TODO: allow users to adjust mappings
|
|
||||||
mappings=original_job.mappings,
|
|
||||||
retry=True,
|
retry=True,
|
||||||
)
|
)
|
||||||
for item in items:
|
for item in items:
|
||||||
# this will re-normalize the raw data
|
self.save_item(job, item.index, item.data)
|
||||||
self.create_item(job, item.index, item.data)
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
def start_import(self, job): # pylint: disable=no-self-use
|
def start_import(self, job):
|
||||||
"""initalizes a csv import job"""
|
"""initalizes a csv import job"""
|
||||||
result = start_import_task.delay(job.id)
|
result = import_data.delay(self.service, job.id)
|
||||||
job.task_id = result.id
|
job.task_id = result.id
|
||||||
job.save()
|
job.save()
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue="low_priority")
|
@app.task(queue="low_priority")
|
||||||
def start_import_task(job_id):
|
def import_data(source, job_id):
|
||||||
"""trigger the child tasks for each row"""
|
"""does the actual lookup work in a celery task"""
|
||||||
job = ImportJob.objects.get(id=job_id)
|
job = ImportJob.objects.get(id=job_id)
|
||||||
# these are sub-tasks so that one big task doesn't use up all the memory in celery
|
try:
|
||||||
for item in job.items.values_list("id", flat=True).all():
|
for item in job.items.all():
|
||||||
import_item_task.delay(item)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue="low_priority")
|
|
||||||
def import_item_task(item_id):
|
|
||||||
"""resolve a row into a book"""
|
|
||||||
item = models.ImportItem.objects.get(id=item_id)
|
|
||||||
try:
|
try:
|
||||||
item.resolve()
|
item.resolve()
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
logger.exception(err)
|
||||||
item.fail_reason = _("Error loading book")
|
item.fail_reason = _("Error loading book")
|
||||||
item.save()
|
item.save()
|
||||||
item.update_job()
|
continue
|
||||||
raise err
|
|
||||||
|
if item.book or item.book_guess:
|
||||||
|
item.save()
|
||||||
|
|
||||||
if item.book:
|
if item.book:
|
||||||
# shelves book and handles reviews
|
# shelves book and handles reviews
|
||||||
handle_imported_book(item)
|
handle_imported_book(
|
||||||
|
source, job.user, item, job.include_reviews, job.privacy
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
item.fail_reason = _("Could not find a match for book")
|
item.fail_reason = _("Could not find a match for book")
|
||||||
|
|
||||||
item.save()
|
item.save()
|
||||||
item.update_job()
|
finally:
|
||||||
|
job.complete = True
|
||||||
|
job.save()
|
||||||
|
|
||||||
|
|
||||||
def handle_imported_book(item):
|
def handle_imported_book(source, user, item, include_reviews, privacy):
|
||||||
"""process a csv and then post about it"""
|
"""process a csv and then post about it"""
|
||||||
job = item.job
|
|
||||||
user = job.user
|
|
||||||
if isinstance(item.book, models.Work):
|
if isinstance(item.book, models.Work):
|
||||||
item.book = item.book.default_edition
|
item.book = item.book.default_edition
|
||||||
if not item.book:
|
if not item.book:
|
||||||
item.fail_reason = _("Error loading book")
|
|
||||||
item.save()
|
|
||||||
return
|
return
|
||||||
if not isinstance(item.book, models.Edition):
|
|
||||||
item.book = item.book.edition
|
|
||||||
|
|
||||||
existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists()
|
existing_shelf = models.ShelfBook.objects.filter(book=item.book, user=user).exists()
|
||||||
|
|
||||||
|
@ -176,9 +105,9 @@ def handle_imported_book(item):
|
||||||
if item.shelf and not existing_shelf:
|
if item.shelf and not existing_shelf:
|
||||||
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user)
|
desired_shelf = models.Shelf.objects.get(identifier=item.shelf, user=user)
|
||||||
shelved_date = item.date_added or timezone.now()
|
shelved_date = item.date_added or timezone.now()
|
||||||
models.ShelfBook(
|
models.ShelfBook.objects.create(
|
||||||
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
|
book=item.book, shelf=desired_shelf, user=user, shelved_date=shelved_date
|
||||||
).save(priority=LOW)
|
)
|
||||||
|
|
||||||
for read in item.reads:
|
for read in item.reads:
|
||||||
# check for an existing readthrough with the same dates
|
# check for an existing readthrough with the same dates
|
||||||
|
@ -193,52 +122,35 @@ def handle_imported_book(item):
|
||||||
read.user = user
|
read.user = user
|
||||||
read.save()
|
read.save()
|
||||||
|
|
||||||
if job.include_reviews and (item.rating or item.review) and not item.linked_review:
|
if include_reviews and (item.rating or item.review):
|
||||||
# we don't know the publication date of the review,
|
# we don't know the publication date of the review,
|
||||||
# but "now" is a bad guess
|
# but "now" is a bad guess
|
||||||
published_date_guess = item.date_read or item.date_added
|
published_date_guess = item.date_read or item.date_added
|
||||||
if item.review:
|
if item.review:
|
||||||
# pylint: disable=consider-using-f-string
|
# pylint: disable=consider-using-f-string
|
||||||
review_title = "Review of {!r} on {!r}".format(
|
review_title = (
|
||||||
|
"Review of {!r} on {!r}".format(
|
||||||
item.book.title,
|
item.book.title,
|
||||||
job.source,
|
source,
|
||||||
)
|
)
|
||||||
review = models.Review.objects.filter(
|
if item.review
|
||||||
user=user,
|
else ""
|
||||||
book=item.book,
|
)
|
||||||
name=review_title,
|
models.Review.objects.create(
|
||||||
rating=item.rating,
|
|
||||||
published_date=published_date_guess,
|
|
||||||
).first()
|
|
||||||
if not review:
|
|
||||||
review = models.Review(
|
|
||||||
user=user,
|
user=user,
|
||||||
book=item.book,
|
book=item.book,
|
||||||
name=review_title,
|
name=review_title,
|
||||||
content=item.review,
|
content=item.review,
|
||||||
rating=item.rating,
|
rating=item.rating,
|
||||||
published_date=published_date_guess,
|
published_date=published_date_guess,
|
||||||
privacy=job.privacy,
|
privacy=privacy,
|
||||||
)
|
)
|
||||||
review.save(software="bookwyrm", priority=LOW)
|
|
||||||
else:
|
else:
|
||||||
# just a rating
|
# just a rating
|
||||||
review = models.ReviewRating.objects.filter(
|
models.ReviewRating.objects.create(
|
||||||
user=user,
|
|
||||||
book=item.book,
|
|
||||||
published_date=published_date_guess,
|
|
||||||
rating=item.rating,
|
|
||||||
).first()
|
|
||||||
if not review:
|
|
||||||
review = models.ReviewRating(
|
|
||||||
user=user,
|
user=user,
|
||||||
book=item.book,
|
book=item.book,
|
||||||
rating=item.rating,
|
rating=item.rating,
|
||||||
published_date=published_date_guess,
|
published_date=published_date_guess,
|
||||||
privacy=job.privacy,
|
privacy=privacy,
|
||||||
)
|
)
|
||||||
review.save(software="bookwyrm", priority=LOW)
|
|
||||||
|
|
||||||
# only broadcast this review to other bookwyrm instances
|
|
||||||
item.linked_review = review
|
|
||||||
item.save()
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
""" handle reading a tsv from librarything """
|
""" handle reading a csv from librarything """
|
||||||
import re
|
import re
|
||||||
|
import math
|
||||||
from bookwyrm.models import Shelf
|
|
||||||
|
|
||||||
from . import Importer
|
from . import Importer
|
||||||
|
|
||||||
|
@ -12,19 +11,32 @@ class LibrarythingImporter(Importer):
|
||||||
service = "LibraryThing"
|
service = "LibraryThing"
|
||||||
delimiter = "\t"
|
delimiter = "\t"
|
||||||
encoding = "ISO-8859-1"
|
encoding = "ISO-8859-1"
|
||||||
|
# mandatory_fields : fields matching the book title and author
|
||||||
|
mandatory_fields = ["Title", "Primary Author"]
|
||||||
|
|
||||||
def normalize_row(self, entry, mappings): # pylint: disable=no-self-use
|
def parse_fields(self, entry):
|
||||||
"""use the dataclass to create the formatted row of data"""
|
"""custom parsing for librarything"""
|
||||||
remove_brackets = lambda v: re.sub(r"\[|\]", "", v) if v else None
|
data = {}
|
||||||
normalized = {k: remove_brackets(entry.get(v)) for k, v in mappings.items()}
|
data["import_source"] = self.service
|
||||||
isbn_13 = normalized.get("isbn_13")
|
data["Book Id"] = entry["Book Id"]
|
||||||
isbn_13 = isbn_13.split(", ") if isbn_13 else []
|
data["Title"] = entry["Title"]
|
||||||
normalized["isbn_13"] = isbn_13[1] if len(isbn_13) > 0 else None
|
data["Author"] = entry["Primary Author"]
|
||||||
return normalized
|
data["ISBN13"] = entry["ISBN"]
|
||||||
|
data["My Review"] = entry["Review"]
|
||||||
|
if entry["Rating"]:
|
||||||
|
data["My Rating"] = math.ceil(float(entry["Rating"]))
|
||||||
|
else:
|
||||||
|
data["My Rating"] = ""
|
||||||
|
data["Date Added"] = re.sub(r"\[|\]", "", entry["Entry Date"])
|
||||||
|
data["Date Started"] = re.sub(r"\[|\]", "", entry["Date Started"])
|
||||||
|
data["Date Read"] = re.sub(r"\[|\]", "", entry["Date Read"])
|
||||||
|
|
||||||
def get_shelf(self, normalized_row):
|
data["Exclusive Shelf"] = None
|
||||||
if normalized_row["date_finished"]:
|
if data["Date Read"]:
|
||||||
return Shelf.READ_FINISHED
|
data["Exclusive Shelf"] = "read"
|
||||||
if normalized_row["date_started"]:
|
elif data["Date Started"]:
|
||||||
return Shelf.READING
|
data["Exclusive Shelf"] = "reading"
|
||||||
return Shelf.TO_READ
|
else:
|
||||||
|
data["Exclusive Shelf"] = "to-read"
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
""" handle reading a csv from openlibrary"""
|
|
||||||
from . import Importer
|
|
||||||
|
|
||||||
|
|
||||||
class OpenLibraryImporter(Importer):
|
|
||||||
"""csv downloads from OpenLibrary"""
|
|
||||||
|
|
||||||
service = "OpenLibrary"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.row_mappings_guesses.append(("openlibrary_key", ["edition id"]))
|
|
||||||
self.row_mappings_guesses.append(("openlibrary_work_key", ["work id"]))
|
|
||||||
super().__init__(*args, **kwargs)
|
|
|
@ -1,8 +1,34 @@
|
||||||
""" handle reading a csv from storygraph"""
|
""" handle reading a csv from librarything """
|
||||||
|
import re
|
||||||
|
import math
|
||||||
|
|
||||||
from . import Importer
|
from . import Importer
|
||||||
|
|
||||||
|
|
||||||
class StorygraphImporter(Importer):
|
class StorygraphImporter(Importer):
|
||||||
"""csv downloads from Storygraph"""
|
"""csv downloads from librarything"""
|
||||||
|
|
||||||
service = "Storygraph"
|
service = "Storygraph"
|
||||||
|
# mandatory_fields : fields matching the book title and author
|
||||||
|
mandatory_fields = ["Title"]
|
||||||
|
|
||||||
|
def parse_fields(self, entry):
|
||||||
|
"""custom parsing for storygraph"""
|
||||||
|
data = {}
|
||||||
|
data["import_source"] = self.service
|
||||||
|
data["Title"] = entry["Title"]
|
||||||
|
data["Author"] = entry["Authors"] if "Authors" in entry else entry["Author"]
|
||||||
|
data["ISBN13"] = entry["ISBN"]
|
||||||
|
data["My Review"] = entry["Review"]
|
||||||
|
if entry["Star Rating"]:
|
||||||
|
data["My Rating"] = math.ceil(float(entry["Star Rating"]))
|
||||||
|
else:
|
||||||
|
data["My Rating"] = ""
|
||||||
|
|
||||||
|
data["Date Added"] = re.sub(r"[/]", "-", entry["Date Added"])
|
||||||
|
data["Date Read"] = re.sub(r"[/]", "-", entry["Last Date Read"])
|
||||||
|
|
||||||
|
data["Exclusive Shelf"] = (
|
||||||
|
{"read": "read", "currently-reading": "reading", "to-read": "to-read"}
|
||||||
|
).get(entry["Read Status"], None)
|
||||||
|
return data
|
||||||
|
|
|
@ -1,251 +0,0 @@
|
||||||
""" access the list streams stored in redis """
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import signals, Count, Q
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.redis_store import RedisStore
|
|
||||||
from bookwyrm.tasks import app, MEDIUM, HIGH
|
|
||||||
|
|
||||||
|
|
||||||
class ListsStream(RedisStore):
|
|
||||||
"""all the lists you can see"""
|
|
||||||
|
|
||||||
def stream_id(self, user): # pylint: disable=no-self-use
|
|
||||||
"""the redis key for this user's instance of this stream"""
|
|
||||||
if isinstance(user, int):
|
|
||||||
# allows the function to take an int or an obj
|
|
||||||
return f"{user}-lists"
|
|
||||||
return f"{user.id}-lists"
|
|
||||||
|
|
||||||
def get_rank(self, obj): # pylint: disable=no-self-use
|
|
||||||
"""lists are sorted by updated date"""
|
|
||||||
return obj.updated_date.timestamp()
|
|
||||||
|
|
||||||
def add_list(self, book_list):
|
|
||||||
"""add a list to users' feeds"""
|
|
||||||
# the pipeline contains all the add-to-stream activities
|
|
||||||
self.add_object_to_related_stores(book_list)
|
|
||||||
|
|
||||||
def add_user_lists(self, viewer, user):
|
|
||||||
"""add a user's lists to another user's feed"""
|
|
||||||
# only add the lists that the viewer should be able to see
|
|
||||||
lists = models.List.privacy_filter(viewer).filter(user=user)
|
|
||||||
self.bulk_add_objects_to_store(lists, self.stream_id(viewer))
|
|
||||||
|
|
||||||
def remove_user_lists(self, viewer, user, exclude_privacy=None):
|
|
||||||
"""remove a user's list from another user's feed"""
|
|
||||||
# remove all so that followers only lists are removed
|
|
||||||
lists = user.list_set
|
|
||||||
if exclude_privacy:
|
|
||||||
lists = lists.exclude(privacy=exclude_privacy)
|
|
||||||
self.bulk_remove_objects_from_store(lists.all(), self.stream_id(viewer))
|
|
||||||
|
|
||||||
def get_list_stream(self, user):
|
|
||||||
"""load the lists to be displayed"""
|
|
||||||
lists = self.get_store(self.stream_id(user))
|
|
||||||
return (
|
|
||||||
models.List.objects.filter(id__in=lists)
|
|
||||||
.annotate(item_count=Count("listitem", filter=Q(listitem__approved=True)))
|
|
||||||
# hide lists with no approved books
|
|
||||||
.filter(item_count__gt=0)
|
|
||||||
.select_related("user")
|
|
||||||
.prefetch_related("listitem_set")
|
|
||||||
.order_by("-updated_date")
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
def populate_lists(self, user):
|
|
||||||
"""go from zero to a timeline"""
|
|
||||||
self.populate_store(self.stream_id(user))
|
|
||||||
|
|
||||||
def get_audience(self, book_list): # pylint: disable=no-self-use
|
|
||||||
"""given a list, what users should see it"""
|
|
||||||
# everybody who could plausibly see this list
|
|
||||||
audience = models.User.objects.filter(
|
|
||||||
is_active=True,
|
|
||||||
local=True, # we only create feeds for users of this instance
|
|
||||||
).exclude( # not blocked
|
|
||||||
Q(id__in=book_list.user.blocks.all()) | Q(blocks=book_list.user)
|
|
||||||
)
|
|
||||||
|
|
||||||
group = book_list.group
|
|
||||||
# only visible to the poster and mentioned users
|
|
||||||
if book_list.privacy == "direct":
|
|
||||||
if group:
|
|
||||||
audience = audience.filter(
|
|
||||||
Q(id=book_list.user.id) # if the user is the post's author
|
|
||||||
| ~Q(groups=group.memberships) # if the user is in the group
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
audience = audience.filter(
|
|
||||||
Q(id=book_list.user.id) # if the user is the post's author
|
|
||||||
)
|
|
||||||
# only visible to the poster's followers and tagged users
|
|
||||||
elif book_list.privacy == "followers":
|
|
||||||
if group:
|
|
||||||
audience = audience.filter(
|
|
||||||
Q(id=book_list.user.id) # if the user is the list's owner
|
|
||||||
| Q(following=book_list.user) # if the user is following the pwmer
|
|
||||||
# if a user is in the group
|
|
||||||
| Q(memberships__group__id=book_list.group.id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
audience = audience.filter(
|
|
||||||
Q(id=book_list.user.id) # if the user is the list's owner
|
|
||||||
| Q(following=book_list.user) # if the user is following the pwmer
|
|
||||||
)
|
|
||||||
return audience.distinct()
|
|
||||||
|
|
||||||
def get_stores_for_object(self, obj):
|
|
||||||
return [self.stream_id(u) for u in self.get_audience(obj)]
|
|
||||||
|
|
||||||
def get_lists_for_user(self, user): # pylint: disable=no-self-use
|
|
||||||
"""given a user, what lists should they see on this stream"""
|
|
||||||
return models.List.privacy_filter(
|
|
||||||
user,
|
|
||||||
privacy_levels=["public", "followers"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_objects_for_store(self, store):
|
|
||||||
user = models.User.objects.get(id=store.split("-")[0])
|
|
||||||
return self.get_lists_for_user(user)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.List)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def add_list_on_create(sender, instance, created, *args, **kwargs):
|
|
||||||
"""add newly created lists streamsstreams"""
|
|
||||||
if not created:
|
|
||||||
return
|
|
||||||
# when creating new things, gotta wait on the transaction
|
|
||||||
transaction.on_commit(lambda: add_list_on_create_command(instance.id))
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_delete, sender=models.List)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def remove_list_on_delete(sender, instance, *args, **kwargs):
|
|
||||||
"""remove deleted lists to streams"""
|
|
||||||
remove_list_task.delay(instance.id)
|
|
||||||
|
|
||||||
|
|
||||||
def add_list_on_create_command(instance_id):
|
|
||||||
"""runs this code only after the database commit completes"""
|
|
||||||
add_list_task.delay(instance_id)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.UserFollows)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def add_lists_on_follow(sender, instance, created, *args, **kwargs):
|
|
||||||
"""add a newly followed user's lists to feeds"""
|
|
||||||
if not created or not instance.user_subject.local:
|
|
||||||
return
|
|
||||||
add_user_lists_task.delay(instance.user_subject.id, instance.user_object.id)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_delete, sender=models.UserFollows)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def remove_lists_on_unfollow(sender, instance, *args, **kwargs):
|
|
||||||
"""remove lists from a feed on unfollow"""
|
|
||||||
if not instance.user_subject.local:
|
|
||||||
return
|
|
||||||
# remove all but public lists
|
|
||||||
remove_user_lists_task.delay(
|
|
||||||
instance.user_subject.id, instance.user_object.id, exclude_privacy="public"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.UserBlocks)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def remove_lists_on_block(sender, instance, *args, **kwargs):
|
|
||||||
"""remove lists from all feeds on block"""
|
|
||||||
# blocks apply ot all feeds
|
|
||||||
if instance.user_subject.local:
|
|
||||||
remove_user_lists_task.delay(instance.user_subject.id, instance.user_object.id)
|
|
||||||
|
|
||||||
# and in both directions
|
|
||||||
if instance.user_object.local:
|
|
||||||
remove_user_lists_task.delay(instance.user_object.id, instance.user_subject.id)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_delete, sender=models.UserBlocks)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def add_lists_on_unblock(sender, instance, *args, **kwargs):
|
|
||||||
"""add lists back to all feeds on unblock"""
|
|
||||||
# make sure there isn't a block in the other direction
|
|
||||||
if models.UserBlocks.objects.filter(
|
|
||||||
user_subject=instance.user_object,
|
|
||||||
user_object=instance.user_subject,
|
|
||||||
).exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
# add lists back to streams with lists from anyone
|
|
||||||
if instance.user_subject.local:
|
|
||||||
add_user_lists_task.delay(
|
|
||||||
instance.user_subject.id,
|
|
||||||
instance.user_object.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# add lists back to streams with lists from anyone
|
|
||||||
if instance.user_object.local:
|
|
||||||
add_user_lists_task.delay(
|
|
||||||
instance.user_object.id,
|
|
||||||
instance.user_subject.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.post_save, sender=models.User)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def populate_lists_on_account_create(sender, instance, created, *args, **kwargs):
|
|
||||||
"""build a user's feeds when they join"""
|
|
||||||
if not created or not instance.local:
|
|
||||||
return
|
|
||||||
transaction.on_commit(lambda: add_list_on_account_create_command(instance.id))
|
|
||||||
|
|
||||||
|
|
||||||
def add_list_on_account_create_command(user_id):
|
|
||||||
"""wait for the transaction to complete"""
|
|
||||||
populate_lists_task.delay(user_id)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- TASKS
|
|
||||||
@app.task(queue=MEDIUM)
|
|
||||||
def populate_lists_task(user_id):
|
|
||||||
"""background task for populating an empty list stream"""
|
|
||||||
user = models.User.objects.get(id=user_id)
|
|
||||||
ListsStream().populate_lists(user)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
|
||||||
def remove_list_task(list_id):
|
|
||||||
"""remove a list from any stream it might be in"""
|
|
||||||
stores = models.User.objects.filter(local=True, is_active=True).values_list(
|
|
||||||
"id", flat=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# delete for every store
|
|
||||||
stores = [ListsStream().stream_id(idx) for idx in stores]
|
|
||||||
ListsStream().remove_object_from_related_stores(list_id, stores=stores)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=HIGH)
|
|
||||||
def add_list_task(list_id):
|
|
||||||
"""add a list to any stream it should be in"""
|
|
||||||
book_list = models.List.objects.get(id=list_id)
|
|
||||||
ListsStream().add_list(book_list)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
|
||||||
def remove_user_lists_task(viewer_id, user_id, exclude_privacy=None):
|
|
||||||
"""remove all lists by a user from a viewer's stream"""
|
|
||||||
viewer = models.User.objects.get(id=viewer_id)
|
|
||||||
user = models.User.objects.get(id=user_id)
|
|
||||||
ListsStream().remove_user_lists(viewer, user, exclude_privacy=exclude_privacy)
|
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MEDIUM)
|
|
||||||
def add_user_lists_task(viewer_id, user_id):
|
|
||||||
"""add all lists by a user to a viewer's stream"""
|
|
||||||
viewer = models.User.objects.get(id=viewer_id)
|
|
||||||
user = models.User.objects.get(id=user_id)
|
|
||||||
ListsStream().add_user_lists(viewer, user)
|
|
|
@ -1,23 +0,0 @@
|
||||||
""" Get your admin code to allow install """
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
|
|
||||||
|
|
||||||
def get_admin_code():
|
|
||||||
"""get that code"""
|
|
||||||
return models.SiteSettings.objects.get().admin_code
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
"""command-line options"""
|
|
||||||
|
|
||||||
help = "Gets admin code for configuring BookWyrm"
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
"""execute init"""
|
|
||||||
self.stdout.write("*******************************************")
|
|
||||||
self.stdout.write("Use this code to create your admin account:")
|
|
||||||
self.stdout.write(get_admin_code())
|
|
||||||
self.stdout.write("*******************************************")
|
|
|
@ -5,10 +5,7 @@ import redis
|
||||||
from bookwyrm import settings
|
from bookwyrm import settings
|
||||||
|
|
||||||
r = redis.Redis(
|
r = redis.Redis(
|
||||||
host=settings.REDIS_ACTIVITY_HOST,
|
host=settings.REDIS_ACTIVITY_HOST, port=settings.REDIS_ACTIVITY_PORT, db=0
|
||||||
port=settings.REDIS_ACTIVITY_PORT,
|
|
||||||
password=settings.REDIS_ACTIVITY_PASSWORD,
|
|
||||||
db=settings.REDIS_ACTIVITY_DB_INDEX,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
help = "Generate preview images"
|
help = "Generate preview images"
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
"""options for how the command is run"""
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--all",
|
"--all",
|
||||||
"-a",
|
"-a",
|
||||||
|
@ -40,7 +38,6 @@ class Command(BaseCommand):
|
||||||
preview_images.generate_site_preview_image_task.delay()
|
preview_images.generate_site_preview_image_task.delay()
|
||||||
self.stdout.write(" OK 🖼")
|
self.stdout.write(" OK 🖼")
|
||||||
|
|
||||||
# pylint: disable=consider-using-f-string
|
|
||||||
if options["all"]:
|
if options["all"]:
|
||||||
# Users
|
# Users
|
||||||
users = models.User.objects.filter(
|
users = models.User.objects.filter(
|
||||||
|
@ -56,17 +53,12 @@ class Command(BaseCommand):
|
||||||
self.stdout.write(" OK 🖼")
|
self.stdout.write(" OK 🖼")
|
||||||
|
|
||||||
# Books
|
# Books
|
||||||
book_ids = (
|
books = models.Book.objects.select_subclasses().filter()
|
||||||
models.Book.objects.select_subclasses()
|
|
||||||
.filter()
|
|
||||||
.values_list("id", flat=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
" → Book preview images ({}): ".format(len(book_ids)), ending=""
|
" → Book preview images ({}): ".format(len(books)), ending=""
|
||||||
)
|
)
|
||||||
for book_id in book_ids:
|
for book in books:
|
||||||
preview_images.generate_edition_preview_image_task.delay(book_id)
|
preview_images.generate_edition_preview_image_task.delay(book.id)
|
||||||
self.stdout.write(".", ending="")
|
self.stdout.write(".", ending="")
|
||||||
self.stdout.write(" OK 🖼")
|
self.stdout.write(" OK 🖼")
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand
|
||||||
from django.contrib.auth.models import Group, Permission
|
from django.contrib.auth.models import Group, Permission
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm.models import Connector, FederatedServer, SiteSettings, User
|
||||||
|
|
||||||
|
|
||||||
def init_groups():
|
def init_groups():
|
||||||
|
@ -19,7 +19,9 @@ def init_permissions():
|
||||||
{
|
{
|
||||||
"codename": "edit_instance_settings",
|
"codename": "edit_instance_settings",
|
||||||
"name": "change the instance info",
|
"name": "change the instance info",
|
||||||
"groups": ["admin"],
|
"groups": [
|
||||||
|
"admin",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"codename": "set_user_group",
|
"codename": "set_user_group",
|
||||||
|
@ -53,7 +55,7 @@ def init_permissions():
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
content_type = ContentType.objects.get_for_model(models.User)
|
content_type = ContentType.objects.get_for_model(User)
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
permission_obj = Permission.objects.create(
|
permission_obj = Permission.objects.create(
|
||||||
codename=permission["codename"],
|
codename=permission["codename"],
|
||||||
|
@ -64,12 +66,15 @@ def init_permissions():
|
||||||
for group_name in permission["groups"]:
|
for group_name in permission["groups"]:
|
||||||
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
Group.objects.get(name=group_name).permissions.add(permission_obj)
|
||||||
|
|
||||||
|
# while the groups and permissions shouldn't be changed because the code
|
||||||
|
# depends on them, what permissions go with what groups should be editable
|
||||||
|
|
||||||
|
|
||||||
def init_connectors():
|
def init_connectors():
|
||||||
"""access book data sources"""
|
"""access book data sources"""
|
||||||
models.Connector.objects.create(
|
Connector.objects.create(
|
||||||
identifier="bookwyrm.social",
|
identifier="bookwyrm.social",
|
||||||
name="Bookwyrm.social",
|
name="BookWyrm dot Social",
|
||||||
connector_file="bookwyrm_connector",
|
connector_file="bookwyrm_connector",
|
||||||
base_url="https://bookwyrm.social",
|
base_url="https://bookwyrm.social",
|
||||||
books_url="https://bookwyrm.social/book",
|
books_url="https://bookwyrm.social/book",
|
||||||
|
@ -79,8 +84,7 @@ def init_connectors():
|
||||||
priority=2,
|
priority=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable=line-too-long
|
Connector.objects.create(
|
||||||
models.Connector.objects.create(
|
|
||||||
identifier="inventaire.io",
|
identifier="inventaire.io",
|
||||||
name="Inventaire",
|
name="Inventaire",
|
||||||
connector_file="inventaire",
|
connector_file="inventaire",
|
||||||
|
@ -89,10 +93,10 @@ def init_connectors():
|
||||||
covers_url="https://inventaire.io",
|
covers_url="https://inventaire.io",
|
||||||
search_url="https://inventaire.io/api/search?types=works&types=works&search=",
|
search_url="https://inventaire.io/api/search?types=works&types=works&search=",
|
||||||
isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A",
|
isbn_search_url="https://inventaire.io/api/entities?action=by-uris&uris=isbn%3A",
|
||||||
priority=1,
|
priority=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
models.Connector.objects.create(
|
Connector.objects.create(
|
||||||
identifier="openlibrary.org",
|
identifier="openlibrary.org",
|
||||||
name="OpenLibrary",
|
name="OpenLibrary",
|
||||||
connector_file="openlibrary",
|
connector_file="openlibrary",
|
||||||
|
@ -101,71 +105,34 @@ def init_connectors():
|
||||||
covers_url="https://covers.openlibrary.org",
|
covers_url="https://covers.openlibrary.org",
|
||||||
search_url="https://openlibrary.org/search?q=",
|
search_url="https://openlibrary.org/search?q=",
|
||||||
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
isbn_search_url="https://openlibrary.org/api/books?jscmd=data&format=json&bibkeys=ISBN:",
|
||||||
priority=1,
|
priority=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def init_federated_servers():
|
||||||
|
"""big no to nazis"""
|
||||||
|
built_in_blocks = ["gab.ai", "gab.com"]
|
||||||
|
for server in built_in_blocks:
|
||||||
|
FederatedServer.objects.create(
|
||||||
|
server_name=server,
|
||||||
|
status="blocked",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init_settings():
|
def init_settings():
|
||||||
"""info about the instance"""
|
"""info about the instance"""
|
||||||
models.SiteSettings.objects.create(
|
SiteSettings.objects.create(
|
||||||
support_link="https://www.patreon.com/bookwyrm",
|
support_link="https://www.patreon.com/bookwyrm",
|
||||||
support_title="Patreon",
|
support_title="Patreon",
|
||||||
install_mode=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init_link_domains():
|
|
||||||
"""safe book links"""
|
|
||||||
domains = [
|
|
||||||
("standardebooks.org", "Standard EBooks"),
|
|
||||||
("www.gutenberg.org", "Project Gutenberg"),
|
|
||||||
("archive.org", "Internet Archive"),
|
|
||||||
("openlibrary.org", "Open Library"),
|
|
||||||
("theanarchistlibrary.org", "The Anarchist Library"),
|
|
||||||
]
|
|
||||||
for domain, name in domains:
|
|
||||||
models.LinkDomain.objects.create(
|
|
||||||
domain=domain,
|
|
||||||
name=name,
|
|
||||||
status="approved",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""command-line options"""
|
|
||||||
|
|
||||||
help = "Initializes the database with starter data"
|
help = "Initializes the database with starter data"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
"""specify which function to run"""
|
|
||||||
parser.add_argument(
|
|
||||||
"--limit",
|
|
||||||
default=None,
|
|
||||||
help="Limit init to specific table",
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
"""execute init"""
|
|
||||||
limit = options.get("limit")
|
|
||||||
tables = [
|
|
||||||
"group",
|
|
||||||
"permission",
|
|
||||||
"connector",
|
|
||||||
"settings",
|
|
||||||
"linkdomain",
|
|
||||||
]
|
|
||||||
if limit and limit not in tables:
|
|
||||||
raise Exception("Invalid table limit:", limit)
|
|
||||||
|
|
||||||
if not limit or limit == "group":
|
|
||||||
init_groups()
|
init_groups()
|
||||||
if not limit or limit == "permission":
|
|
||||||
init_permissions()
|
init_permissions()
|
||||||
if not limit or limit == "connector":
|
|
||||||
init_connectors()
|
init_connectors()
|
||||||
if not limit or limit == "settings":
|
init_federated_servers()
|
||||||
init_settings()
|
init_settings()
|
||||||
if not limit or limit == "linkdomain":
|
|
||||||
init_link_domains()
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
""" Get your admin code to allow install """
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from bookwyrm import models
|
|
||||||
from bookwyrm.settings import VERSION
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
class Command(BaseCommand):
|
|
||||||
"""command-line options"""
|
|
||||||
|
|
||||||
help = "What version is this?"
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
"""specify which function to run"""
|
|
||||||
parser.add_argument(
|
|
||||||
"--current",
|
|
||||||
action="store_true",
|
|
||||||
help="Version stored in database",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--target",
|
|
||||||
action="store_true",
|
|
||||||
help="Version stored in settings",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--update",
|
|
||||||
action="store_true",
|
|
||||||
help="Update database version",
|
|
||||||
)
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
"""execute init"""
|
|
||||||
site = models.SiteSettings.objects.get()
|
|
||||||
current = site.version or "0.0.1"
|
|
||||||
target = VERSION
|
|
||||||
if options.get("current"):
|
|
||||||
print(current)
|
|
||||||
return
|
|
||||||
|
|
||||||
if options.get("target"):
|
|
||||||
print(target)
|
|
||||||
return
|
|
||||||
|
|
||||||
if options.get("update"):
|
|
||||||
site.version = target
|
|
||||||
site.save()
|
|
||||||
return
|
|
||||||
|
|
||||||
if current != target:
|
|
||||||
print(f"{current}/{target}")
|
|
||||||
else:
|
|
||||||
print(current)
|
|
|
@ -1,28 +0,0 @@
|
||||||
""" Re-create list streams """
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from bookwyrm import lists_stream, models
|
|
||||||
|
|
||||||
|
|
||||||
def populate_lists_streams():
|
|
||||||
"""build all the lists streams for all the users"""
|
|
||||||
print("Populating lists streams")
|
|
||||||
users = models.User.objects.filter(
|
|
||||||
local=True,
|
|
||||||
is_active=True,
|
|
||||||
).order_by("-last_active_date")
|
|
||||||
print("This may take a long time! Please be patient.")
|
|
||||||
for user in users:
|
|
||||||
print(".", end="")
|
|
||||||
lists_stream.populate_lists_task.delay(user.id)
|
|
||||||
print("\nAll done, thank you for your patience!")
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
"""start all over with lists streams"""
|
|
||||||
|
|
||||||
help = "Populate list streams for all users"
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use,unused-argument
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
"""run feed builder"""
|
|
||||||
populate_lists_streams()
|
|
|
@ -1,20 +1,18 @@
|
||||||
""" Re-create user streams """
|
""" Re-create user streams """
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from bookwyrm import activitystreams, lists_stream, models
|
from bookwyrm import activitystreams, models
|
||||||
|
|
||||||
|
|
||||||
def populate_streams(stream=None):
|
def populate_streams(stream=None):
|
||||||
"""build all the streams for all the users"""
|
"""build all the streams for all the users"""
|
||||||
streams = [stream] if stream else activitystreams.streams.keys()
|
streams = [stream] if stream else activitystreams.streams.keys()
|
||||||
print("Populating streams", streams)
|
print("Populations streams", streams)
|
||||||
users = models.User.objects.filter(
|
users = models.User.objects.filter(
|
||||||
local=True,
|
local=True,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).order_by("-last_active_date")
|
).order_by("-last_active_date")
|
||||||
print("This may take a long time! Please be patient.")
|
print("This may take a long time! Please be patient.")
|
||||||
for user in users:
|
for user in users:
|
||||||
print(".", end="")
|
|
||||||
lists_stream.populate_lists_task.delay(user.id)
|
|
||||||
for stream_key in streams:
|
for stream_key in streams:
|
||||||
print(".", end="")
|
print(".", end="")
|
||||||
activitystreams.populate_stream_task.delay(stream_key, user.id)
|
activitystreams.populate_stream_task.delay(stream_key, user.id)
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-11 16:22
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0106_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es", "Español (Spanish)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("pt-br", "Português - Brasil (Brazilian Portugues)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,871 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-16 06:39
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0106_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Group",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_date", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
|
||||||
"remote_id",
|
|
||||||
bookwyrm.models.fields.RemoteIdField(
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", bookwyrm.models.fields.CharField(max_length=100)),
|
|
||||||
(
|
|
||||||
"description",
|
|
||||||
bookwyrm.models.fields.TextField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"privacy",
|
|
||||||
bookwyrm.models.fields.PrivacyField(
|
|
||||||
choices=[
|
|
||||||
("public", "Public"),
|
|
||||||
("unlisted", "Unlisted"),
|
|
||||||
("followers", "Followers"),
|
|
||||||
("direct", "Direct"),
|
|
||||||
],
|
|
||||||
default="public",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="GroupMember",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_date", models.DateTimeField(auto_now=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="GroupMemberInvitation",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.RemoveConstraint(
|
|
||||||
model_name="notification",
|
|
||||||
name="notification_type_valid",
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="list",
|
|
||||||
name="curation",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
choices=[
|
|
||||||
("closed", "Closed"),
|
|
||||||
("open", "Open"),
|
|
||||||
("curated", "Curated"),
|
|
||||||
("group", "Group"),
|
|
||||||
],
|
|
||||||
default="closed",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="notification",
|
|
||||||
name="notification_type",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("FAVORITE", "Favorite"),
|
|
||||||
("REPLY", "Reply"),
|
|
||||||
("MENTION", "Mention"),
|
|
||||||
("TAG", "Tag"),
|
|
||||||
("FOLLOW", "Follow"),
|
|
||||||
("FOLLOW_REQUEST", "Follow Request"),
|
|
||||||
("BOOST", "Boost"),
|
|
||||||
("IMPORT", "Import"),
|
|
||||||
("ADD", "Add"),
|
|
||||||
("REPORT", "Report"),
|
|
||||||
("INVITE", "Invite"),
|
|
||||||
("ACCEPT", "Accept"),
|
|
||||||
("JOIN", "Join"),
|
|
||||||
("LEAVE", "Leave"),
|
|
||||||
("REMOVE", "Remove"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es", "Español (Spanish)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_timezone",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("Africa/Abidjan", "Africa/Abidjan"),
|
|
||||||
("Africa/Accra", "Africa/Accra"),
|
|
||||||
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
|
||||||
("Africa/Algiers", "Africa/Algiers"),
|
|
||||||
("Africa/Asmara", "Africa/Asmara"),
|
|
||||||
("Africa/Asmera", "Africa/Asmera"),
|
|
||||||
("Africa/Bamako", "Africa/Bamako"),
|
|
||||||
("Africa/Bangui", "Africa/Bangui"),
|
|
||||||
("Africa/Banjul", "Africa/Banjul"),
|
|
||||||
("Africa/Bissau", "Africa/Bissau"),
|
|
||||||
("Africa/Blantyre", "Africa/Blantyre"),
|
|
||||||
("Africa/Brazzaville", "Africa/Brazzaville"),
|
|
||||||
("Africa/Bujumbura", "Africa/Bujumbura"),
|
|
||||||
("Africa/Cairo", "Africa/Cairo"),
|
|
||||||
("Africa/Casablanca", "Africa/Casablanca"),
|
|
||||||
("Africa/Ceuta", "Africa/Ceuta"),
|
|
||||||
("Africa/Conakry", "Africa/Conakry"),
|
|
||||||
("Africa/Dakar", "Africa/Dakar"),
|
|
||||||
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
|
||||||
("Africa/Djibouti", "Africa/Djibouti"),
|
|
||||||
("Africa/Douala", "Africa/Douala"),
|
|
||||||
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
|
||||||
("Africa/Freetown", "Africa/Freetown"),
|
|
||||||
("Africa/Gaborone", "Africa/Gaborone"),
|
|
||||||
("Africa/Harare", "Africa/Harare"),
|
|
||||||
("Africa/Johannesburg", "Africa/Johannesburg"),
|
|
||||||
("Africa/Juba", "Africa/Juba"),
|
|
||||||
("Africa/Kampala", "Africa/Kampala"),
|
|
||||||
("Africa/Khartoum", "Africa/Khartoum"),
|
|
||||||
("Africa/Kigali", "Africa/Kigali"),
|
|
||||||
("Africa/Kinshasa", "Africa/Kinshasa"),
|
|
||||||
("Africa/Lagos", "Africa/Lagos"),
|
|
||||||
("Africa/Libreville", "Africa/Libreville"),
|
|
||||||
("Africa/Lome", "Africa/Lome"),
|
|
||||||
("Africa/Luanda", "Africa/Luanda"),
|
|
||||||
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
|
||||||
("Africa/Lusaka", "Africa/Lusaka"),
|
|
||||||
("Africa/Malabo", "Africa/Malabo"),
|
|
||||||
("Africa/Maputo", "Africa/Maputo"),
|
|
||||||
("Africa/Maseru", "Africa/Maseru"),
|
|
||||||
("Africa/Mbabane", "Africa/Mbabane"),
|
|
||||||
("Africa/Mogadishu", "Africa/Mogadishu"),
|
|
||||||
("Africa/Monrovia", "Africa/Monrovia"),
|
|
||||||
("Africa/Nairobi", "Africa/Nairobi"),
|
|
||||||
("Africa/Ndjamena", "Africa/Ndjamena"),
|
|
||||||
("Africa/Niamey", "Africa/Niamey"),
|
|
||||||
("Africa/Nouakchott", "Africa/Nouakchott"),
|
|
||||||
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
|
||||||
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
|
||||||
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
|
||||||
("Africa/Timbuktu", "Africa/Timbuktu"),
|
|
||||||
("Africa/Tripoli", "Africa/Tripoli"),
|
|
||||||
("Africa/Tunis", "Africa/Tunis"),
|
|
||||||
("Africa/Windhoek", "Africa/Windhoek"),
|
|
||||||
("America/Adak", "America/Adak"),
|
|
||||||
("America/Anchorage", "America/Anchorage"),
|
|
||||||
("America/Anguilla", "America/Anguilla"),
|
|
||||||
("America/Antigua", "America/Antigua"),
|
|
||||||
("America/Araguaina", "America/Araguaina"),
|
|
||||||
(
|
|
||||||
"America/Argentina/Buenos_Aires",
|
|
||||||
"America/Argentina/Buenos_Aires",
|
|
||||||
),
|
|
||||||
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
|
|
||||||
(
|
|
||||||
"America/Argentina/ComodRivadavia",
|
|
||||||
"America/Argentina/ComodRivadavia",
|
|
||||||
),
|
|
||||||
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
|
||||||
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
|
||||||
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
|
||||||
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
|
||||||
(
|
|
||||||
"America/Argentina/Rio_Gallegos",
|
|
||||||
"America/Argentina/Rio_Gallegos",
|
|
||||||
),
|
|
||||||
("America/Argentina/Salta", "America/Argentina/Salta"),
|
|
||||||
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
|
||||||
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
|
||||||
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
|
||||||
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
|
||||||
("America/Aruba", "America/Aruba"),
|
|
||||||
("America/Asuncion", "America/Asuncion"),
|
|
||||||
("America/Atikokan", "America/Atikokan"),
|
|
||||||
("America/Atka", "America/Atka"),
|
|
||||||
("America/Bahia", "America/Bahia"),
|
|
||||||
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
|
||||||
("America/Barbados", "America/Barbados"),
|
|
||||||
("America/Belem", "America/Belem"),
|
|
||||||
("America/Belize", "America/Belize"),
|
|
||||||
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
|
||||||
("America/Boa_Vista", "America/Boa_Vista"),
|
|
||||||
("America/Bogota", "America/Bogota"),
|
|
||||||
("America/Boise", "America/Boise"),
|
|
||||||
("America/Buenos_Aires", "America/Buenos_Aires"),
|
|
||||||
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
|
||||||
("America/Campo_Grande", "America/Campo_Grande"),
|
|
||||||
("America/Cancun", "America/Cancun"),
|
|
||||||
("America/Caracas", "America/Caracas"),
|
|
||||||
("America/Catamarca", "America/Catamarca"),
|
|
||||||
("America/Cayenne", "America/Cayenne"),
|
|
||||||
("America/Cayman", "America/Cayman"),
|
|
||||||
("America/Chicago", "America/Chicago"),
|
|
||||||
("America/Chihuahua", "America/Chihuahua"),
|
|
||||||
("America/Coral_Harbour", "America/Coral_Harbour"),
|
|
||||||
("America/Cordoba", "America/Cordoba"),
|
|
||||||
("America/Costa_Rica", "America/Costa_Rica"),
|
|
||||||
("America/Creston", "America/Creston"),
|
|
||||||
("America/Cuiaba", "America/Cuiaba"),
|
|
||||||
("America/Curacao", "America/Curacao"),
|
|
||||||
("America/Danmarkshavn", "America/Danmarkshavn"),
|
|
||||||
("America/Dawson", "America/Dawson"),
|
|
||||||
("America/Dawson_Creek", "America/Dawson_Creek"),
|
|
||||||
("America/Denver", "America/Denver"),
|
|
||||||
("America/Detroit", "America/Detroit"),
|
|
||||||
("America/Dominica", "America/Dominica"),
|
|
||||||
("America/Edmonton", "America/Edmonton"),
|
|
||||||
("America/Eirunepe", "America/Eirunepe"),
|
|
||||||
("America/El_Salvador", "America/El_Salvador"),
|
|
||||||
("America/Ensenada", "America/Ensenada"),
|
|
||||||
("America/Fort_Nelson", "America/Fort_Nelson"),
|
|
||||||
("America/Fort_Wayne", "America/Fort_Wayne"),
|
|
||||||
("America/Fortaleza", "America/Fortaleza"),
|
|
||||||
("America/Glace_Bay", "America/Glace_Bay"),
|
|
||||||
("America/Godthab", "America/Godthab"),
|
|
||||||
("America/Goose_Bay", "America/Goose_Bay"),
|
|
||||||
("America/Grand_Turk", "America/Grand_Turk"),
|
|
||||||
("America/Grenada", "America/Grenada"),
|
|
||||||
("America/Guadeloupe", "America/Guadeloupe"),
|
|
||||||
("America/Guatemala", "America/Guatemala"),
|
|
||||||
("America/Guayaquil", "America/Guayaquil"),
|
|
||||||
("America/Guyana", "America/Guyana"),
|
|
||||||
("America/Halifax", "America/Halifax"),
|
|
||||||
("America/Havana", "America/Havana"),
|
|
||||||
("America/Hermosillo", "America/Hermosillo"),
|
|
||||||
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
|
||||||
("America/Indiana/Knox", "America/Indiana/Knox"),
|
|
||||||
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
|
||||||
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
|
||||||
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
|
||||||
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
|
||||||
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
|
||||||
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
|
||||||
("America/Indianapolis", "America/Indianapolis"),
|
|
||||||
("America/Inuvik", "America/Inuvik"),
|
|
||||||
("America/Iqaluit", "America/Iqaluit"),
|
|
||||||
("America/Jamaica", "America/Jamaica"),
|
|
||||||
("America/Jujuy", "America/Jujuy"),
|
|
||||||
("America/Juneau", "America/Juneau"),
|
|
||||||
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
|
|
||||||
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
|
|
||||||
("America/Knox_IN", "America/Knox_IN"),
|
|
||||||
("America/Kralendijk", "America/Kralendijk"),
|
|
||||||
("America/La_Paz", "America/La_Paz"),
|
|
||||||
("America/Lima", "America/Lima"),
|
|
||||||
("America/Los_Angeles", "America/Los_Angeles"),
|
|
||||||
("America/Louisville", "America/Louisville"),
|
|
||||||
("America/Lower_Princes", "America/Lower_Princes"),
|
|
||||||
("America/Maceio", "America/Maceio"),
|
|
||||||
("America/Managua", "America/Managua"),
|
|
||||||
("America/Manaus", "America/Manaus"),
|
|
||||||
("America/Marigot", "America/Marigot"),
|
|
||||||
("America/Martinique", "America/Martinique"),
|
|
||||||
("America/Matamoros", "America/Matamoros"),
|
|
||||||
("America/Mazatlan", "America/Mazatlan"),
|
|
||||||
("America/Mendoza", "America/Mendoza"),
|
|
||||||
("America/Menominee", "America/Menominee"),
|
|
||||||
("America/Merida", "America/Merida"),
|
|
||||||
("America/Metlakatla", "America/Metlakatla"),
|
|
||||||
("America/Mexico_City", "America/Mexico_City"),
|
|
||||||
("America/Miquelon", "America/Miquelon"),
|
|
||||||
("America/Moncton", "America/Moncton"),
|
|
||||||
("America/Monterrey", "America/Monterrey"),
|
|
||||||
("America/Montevideo", "America/Montevideo"),
|
|
||||||
("America/Montreal", "America/Montreal"),
|
|
||||||
("America/Montserrat", "America/Montserrat"),
|
|
||||||
("America/Nassau", "America/Nassau"),
|
|
||||||
("America/New_York", "America/New_York"),
|
|
||||||
("America/Nipigon", "America/Nipigon"),
|
|
||||||
("America/Nome", "America/Nome"),
|
|
||||||
("America/Noronha", "America/Noronha"),
|
|
||||||
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
|
||||||
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
|
||||||
(
|
|
||||||
"America/North_Dakota/New_Salem",
|
|
||||||
"America/North_Dakota/New_Salem",
|
|
||||||
),
|
|
||||||
("America/Nuuk", "America/Nuuk"),
|
|
||||||
("America/Ojinaga", "America/Ojinaga"),
|
|
||||||
("America/Panama", "America/Panama"),
|
|
||||||
("America/Pangnirtung", "America/Pangnirtung"),
|
|
||||||
("America/Paramaribo", "America/Paramaribo"),
|
|
||||||
("America/Phoenix", "America/Phoenix"),
|
|
||||||
("America/Port-au-Prince", "America/Port-au-Prince"),
|
|
||||||
("America/Port_of_Spain", "America/Port_of_Spain"),
|
|
||||||
("America/Porto_Acre", "America/Porto_Acre"),
|
|
||||||
("America/Porto_Velho", "America/Porto_Velho"),
|
|
||||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
|
||||||
("America/Punta_Arenas", "America/Punta_Arenas"),
|
|
||||||
("America/Rainy_River", "America/Rainy_River"),
|
|
||||||
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
|
||||||
("America/Recife", "America/Recife"),
|
|
||||||
("America/Regina", "America/Regina"),
|
|
||||||
("America/Resolute", "America/Resolute"),
|
|
||||||
("America/Rio_Branco", "America/Rio_Branco"),
|
|
||||||
("America/Rosario", "America/Rosario"),
|
|
||||||
("America/Santa_Isabel", "America/Santa_Isabel"),
|
|
||||||
("America/Santarem", "America/Santarem"),
|
|
||||||
("America/Santiago", "America/Santiago"),
|
|
||||||
("America/Santo_Domingo", "America/Santo_Domingo"),
|
|
||||||
("America/Sao_Paulo", "America/Sao_Paulo"),
|
|
||||||
("America/Scoresbysund", "America/Scoresbysund"),
|
|
||||||
("America/Shiprock", "America/Shiprock"),
|
|
||||||
("America/Sitka", "America/Sitka"),
|
|
||||||
("America/St_Barthelemy", "America/St_Barthelemy"),
|
|
||||||
("America/St_Johns", "America/St_Johns"),
|
|
||||||
("America/St_Kitts", "America/St_Kitts"),
|
|
||||||
("America/St_Lucia", "America/St_Lucia"),
|
|
||||||
("America/St_Thomas", "America/St_Thomas"),
|
|
||||||
("America/St_Vincent", "America/St_Vincent"),
|
|
||||||
("America/Swift_Current", "America/Swift_Current"),
|
|
||||||
("America/Tegucigalpa", "America/Tegucigalpa"),
|
|
||||||
("America/Thule", "America/Thule"),
|
|
||||||
("America/Thunder_Bay", "America/Thunder_Bay"),
|
|
||||||
("America/Tijuana", "America/Tijuana"),
|
|
||||||
("America/Toronto", "America/Toronto"),
|
|
||||||
("America/Tortola", "America/Tortola"),
|
|
||||||
("America/Vancouver", "America/Vancouver"),
|
|
||||||
("America/Virgin", "America/Virgin"),
|
|
||||||
("America/Whitehorse", "America/Whitehorse"),
|
|
||||||
("America/Winnipeg", "America/Winnipeg"),
|
|
||||||
("America/Yakutat", "America/Yakutat"),
|
|
||||||
("America/Yellowknife", "America/Yellowknife"),
|
|
||||||
("Antarctica/Casey", "Antarctica/Casey"),
|
|
||||||
("Antarctica/Davis", "Antarctica/Davis"),
|
|
||||||
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
|
||||||
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
|
||||||
("Antarctica/Mawson", "Antarctica/Mawson"),
|
|
||||||
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
|
||||||
("Antarctica/Palmer", "Antarctica/Palmer"),
|
|
||||||
("Antarctica/Rothera", "Antarctica/Rothera"),
|
|
||||||
("Antarctica/South_Pole", "Antarctica/South_Pole"),
|
|
||||||
("Antarctica/Syowa", "Antarctica/Syowa"),
|
|
||||||
("Antarctica/Troll", "Antarctica/Troll"),
|
|
||||||
("Antarctica/Vostok", "Antarctica/Vostok"),
|
|
||||||
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
|
||||||
("Asia/Aden", "Asia/Aden"),
|
|
||||||
("Asia/Almaty", "Asia/Almaty"),
|
|
||||||
("Asia/Amman", "Asia/Amman"),
|
|
||||||
("Asia/Anadyr", "Asia/Anadyr"),
|
|
||||||
("Asia/Aqtau", "Asia/Aqtau"),
|
|
||||||
("Asia/Aqtobe", "Asia/Aqtobe"),
|
|
||||||
("Asia/Ashgabat", "Asia/Ashgabat"),
|
|
||||||
("Asia/Ashkhabad", "Asia/Ashkhabad"),
|
|
||||||
("Asia/Atyrau", "Asia/Atyrau"),
|
|
||||||
("Asia/Baghdad", "Asia/Baghdad"),
|
|
||||||
("Asia/Bahrain", "Asia/Bahrain"),
|
|
||||||
("Asia/Baku", "Asia/Baku"),
|
|
||||||
("Asia/Bangkok", "Asia/Bangkok"),
|
|
||||||
("Asia/Barnaul", "Asia/Barnaul"),
|
|
||||||
("Asia/Beirut", "Asia/Beirut"),
|
|
||||||
("Asia/Bishkek", "Asia/Bishkek"),
|
|
||||||
("Asia/Brunei", "Asia/Brunei"),
|
|
||||||
("Asia/Calcutta", "Asia/Calcutta"),
|
|
||||||
("Asia/Chita", "Asia/Chita"),
|
|
||||||
("Asia/Choibalsan", "Asia/Choibalsan"),
|
|
||||||
("Asia/Chongqing", "Asia/Chongqing"),
|
|
||||||
("Asia/Chungking", "Asia/Chungking"),
|
|
||||||
("Asia/Colombo", "Asia/Colombo"),
|
|
||||||
("Asia/Dacca", "Asia/Dacca"),
|
|
||||||
("Asia/Damascus", "Asia/Damascus"),
|
|
||||||
("Asia/Dhaka", "Asia/Dhaka"),
|
|
||||||
("Asia/Dili", "Asia/Dili"),
|
|
||||||
("Asia/Dubai", "Asia/Dubai"),
|
|
||||||
("Asia/Dushanbe", "Asia/Dushanbe"),
|
|
||||||
("Asia/Famagusta", "Asia/Famagusta"),
|
|
||||||
("Asia/Gaza", "Asia/Gaza"),
|
|
||||||
("Asia/Harbin", "Asia/Harbin"),
|
|
||||||
("Asia/Hebron", "Asia/Hebron"),
|
|
||||||
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
|
||||||
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
|
||||||
("Asia/Hovd", "Asia/Hovd"),
|
|
||||||
("Asia/Irkutsk", "Asia/Irkutsk"),
|
|
||||||
("Asia/Istanbul", "Asia/Istanbul"),
|
|
||||||
("Asia/Jakarta", "Asia/Jakarta"),
|
|
||||||
("Asia/Jayapura", "Asia/Jayapura"),
|
|
||||||
("Asia/Jerusalem", "Asia/Jerusalem"),
|
|
||||||
("Asia/Kabul", "Asia/Kabul"),
|
|
||||||
("Asia/Kamchatka", "Asia/Kamchatka"),
|
|
||||||
("Asia/Karachi", "Asia/Karachi"),
|
|
||||||
("Asia/Kashgar", "Asia/Kashgar"),
|
|
||||||
("Asia/Kathmandu", "Asia/Kathmandu"),
|
|
||||||
("Asia/Katmandu", "Asia/Katmandu"),
|
|
||||||
("Asia/Khandyga", "Asia/Khandyga"),
|
|
||||||
("Asia/Kolkata", "Asia/Kolkata"),
|
|
||||||
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
|
||||||
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
|
||||||
("Asia/Kuching", "Asia/Kuching"),
|
|
||||||
("Asia/Kuwait", "Asia/Kuwait"),
|
|
||||||
("Asia/Macao", "Asia/Macao"),
|
|
||||||
("Asia/Macau", "Asia/Macau"),
|
|
||||||
("Asia/Magadan", "Asia/Magadan"),
|
|
||||||
("Asia/Makassar", "Asia/Makassar"),
|
|
||||||
("Asia/Manila", "Asia/Manila"),
|
|
||||||
("Asia/Muscat", "Asia/Muscat"),
|
|
||||||
("Asia/Nicosia", "Asia/Nicosia"),
|
|
||||||
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
|
||||||
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
|
||||||
("Asia/Omsk", "Asia/Omsk"),
|
|
||||||
("Asia/Oral", "Asia/Oral"),
|
|
||||||
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
|
||||||
("Asia/Pontianak", "Asia/Pontianak"),
|
|
||||||
("Asia/Pyongyang", "Asia/Pyongyang"),
|
|
||||||
("Asia/Qatar", "Asia/Qatar"),
|
|
||||||
("Asia/Qostanay", "Asia/Qostanay"),
|
|
||||||
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
|
||||||
("Asia/Rangoon", "Asia/Rangoon"),
|
|
||||||
("Asia/Riyadh", "Asia/Riyadh"),
|
|
||||||
("Asia/Saigon", "Asia/Saigon"),
|
|
||||||
("Asia/Sakhalin", "Asia/Sakhalin"),
|
|
||||||
("Asia/Samarkand", "Asia/Samarkand"),
|
|
||||||
("Asia/Seoul", "Asia/Seoul"),
|
|
||||||
("Asia/Shanghai", "Asia/Shanghai"),
|
|
||||||
("Asia/Singapore", "Asia/Singapore"),
|
|
||||||
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
|
||||||
("Asia/Taipei", "Asia/Taipei"),
|
|
||||||
("Asia/Tashkent", "Asia/Tashkent"),
|
|
||||||
("Asia/Tbilisi", "Asia/Tbilisi"),
|
|
||||||
("Asia/Tehran", "Asia/Tehran"),
|
|
||||||
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
|
|
||||||
("Asia/Thimbu", "Asia/Thimbu"),
|
|
||||||
("Asia/Thimphu", "Asia/Thimphu"),
|
|
||||||
("Asia/Tokyo", "Asia/Tokyo"),
|
|
||||||
("Asia/Tomsk", "Asia/Tomsk"),
|
|
||||||
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
|
|
||||||
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
|
||||||
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
|
|
||||||
("Asia/Urumqi", "Asia/Urumqi"),
|
|
||||||
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
|
||||||
("Asia/Vientiane", "Asia/Vientiane"),
|
|
||||||
("Asia/Vladivostok", "Asia/Vladivostok"),
|
|
||||||
("Asia/Yakutsk", "Asia/Yakutsk"),
|
|
||||||
("Asia/Yangon", "Asia/Yangon"),
|
|
||||||
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
|
||||||
("Asia/Yerevan", "Asia/Yerevan"),
|
|
||||||
("Atlantic/Azores", "Atlantic/Azores"),
|
|
||||||
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
|
||||||
("Atlantic/Canary", "Atlantic/Canary"),
|
|
||||||
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
|
||||||
("Atlantic/Faeroe", "Atlantic/Faeroe"),
|
|
||||||
("Atlantic/Faroe", "Atlantic/Faroe"),
|
|
||||||
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
|
|
||||||
("Atlantic/Madeira", "Atlantic/Madeira"),
|
|
||||||
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
|
||||||
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
|
||||||
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
|
||||||
("Atlantic/Stanley", "Atlantic/Stanley"),
|
|
||||||
("Australia/ACT", "Australia/ACT"),
|
|
||||||
("Australia/Adelaide", "Australia/Adelaide"),
|
|
||||||
("Australia/Brisbane", "Australia/Brisbane"),
|
|
||||||
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
|
||||||
("Australia/Canberra", "Australia/Canberra"),
|
|
||||||
("Australia/Currie", "Australia/Currie"),
|
|
||||||
("Australia/Darwin", "Australia/Darwin"),
|
|
||||||
("Australia/Eucla", "Australia/Eucla"),
|
|
||||||
("Australia/Hobart", "Australia/Hobart"),
|
|
||||||
("Australia/LHI", "Australia/LHI"),
|
|
||||||
("Australia/Lindeman", "Australia/Lindeman"),
|
|
||||||
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
|
||||||
("Australia/Melbourne", "Australia/Melbourne"),
|
|
||||||
("Australia/NSW", "Australia/NSW"),
|
|
||||||
("Australia/North", "Australia/North"),
|
|
||||||
("Australia/Perth", "Australia/Perth"),
|
|
||||||
("Australia/Queensland", "Australia/Queensland"),
|
|
||||||
("Australia/South", "Australia/South"),
|
|
||||||
("Australia/Sydney", "Australia/Sydney"),
|
|
||||||
("Australia/Tasmania", "Australia/Tasmania"),
|
|
||||||
("Australia/Victoria", "Australia/Victoria"),
|
|
||||||
("Australia/West", "Australia/West"),
|
|
||||||
("Australia/Yancowinna", "Australia/Yancowinna"),
|
|
||||||
("Brazil/Acre", "Brazil/Acre"),
|
|
||||||
("Brazil/DeNoronha", "Brazil/DeNoronha"),
|
|
||||||
("Brazil/East", "Brazil/East"),
|
|
||||||
("Brazil/West", "Brazil/West"),
|
|
||||||
("CET", "CET"),
|
|
||||||
("CST6CDT", "CST6CDT"),
|
|
||||||
("Canada/Atlantic", "Canada/Atlantic"),
|
|
||||||
("Canada/Central", "Canada/Central"),
|
|
||||||
("Canada/Eastern", "Canada/Eastern"),
|
|
||||||
("Canada/Mountain", "Canada/Mountain"),
|
|
||||||
("Canada/Newfoundland", "Canada/Newfoundland"),
|
|
||||||
("Canada/Pacific", "Canada/Pacific"),
|
|
||||||
("Canada/Saskatchewan", "Canada/Saskatchewan"),
|
|
||||||
("Canada/Yukon", "Canada/Yukon"),
|
|
||||||
("Chile/Continental", "Chile/Continental"),
|
|
||||||
("Chile/EasterIsland", "Chile/EasterIsland"),
|
|
||||||
("Cuba", "Cuba"),
|
|
||||||
("EET", "EET"),
|
|
||||||
("EST", "EST"),
|
|
||||||
("EST5EDT", "EST5EDT"),
|
|
||||||
("Egypt", "Egypt"),
|
|
||||||
("Eire", "Eire"),
|
|
||||||
("Etc/GMT", "Etc/GMT"),
|
|
||||||
("Etc/GMT+0", "Etc/GMT+0"),
|
|
||||||
("Etc/GMT+1", "Etc/GMT+1"),
|
|
||||||
("Etc/GMT+10", "Etc/GMT+10"),
|
|
||||||
("Etc/GMT+11", "Etc/GMT+11"),
|
|
||||||
("Etc/GMT+12", "Etc/GMT+12"),
|
|
||||||
("Etc/GMT+2", "Etc/GMT+2"),
|
|
||||||
("Etc/GMT+3", "Etc/GMT+3"),
|
|
||||||
("Etc/GMT+4", "Etc/GMT+4"),
|
|
||||||
("Etc/GMT+5", "Etc/GMT+5"),
|
|
||||||
("Etc/GMT+6", "Etc/GMT+6"),
|
|
||||||
("Etc/GMT+7", "Etc/GMT+7"),
|
|
||||||
("Etc/GMT+8", "Etc/GMT+8"),
|
|
||||||
("Etc/GMT+9", "Etc/GMT+9"),
|
|
||||||
("Etc/GMT-0", "Etc/GMT-0"),
|
|
||||||
("Etc/GMT-1", "Etc/GMT-1"),
|
|
||||||
("Etc/GMT-10", "Etc/GMT-10"),
|
|
||||||
("Etc/GMT-11", "Etc/GMT-11"),
|
|
||||||
("Etc/GMT-12", "Etc/GMT-12"),
|
|
||||||
("Etc/GMT-13", "Etc/GMT-13"),
|
|
||||||
("Etc/GMT-14", "Etc/GMT-14"),
|
|
||||||
("Etc/GMT-2", "Etc/GMT-2"),
|
|
||||||
("Etc/GMT-3", "Etc/GMT-3"),
|
|
||||||
("Etc/GMT-4", "Etc/GMT-4"),
|
|
||||||
("Etc/GMT-5", "Etc/GMT-5"),
|
|
||||||
("Etc/GMT-6", "Etc/GMT-6"),
|
|
||||||
("Etc/GMT-7", "Etc/GMT-7"),
|
|
||||||
("Etc/GMT-8", "Etc/GMT-8"),
|
|
||||||
("Etc/GMT-9", "Etc/GMT-9"),
|
|
||||||
("Etc/GMT0", "Etc/GMT0"),
|
|
||||||
("Etc/Greenwich", "Etc/Greenwich"),
|
|
||||||
("Etc/UCT", "Etc/UCT"),
|
|
||||||
("Etc/UTC", "Etc/UTC"),
|
|
||||||
("Etc/Universal", "Etc/Universal"),
|
|
||||||
("Etc/Zulu", "Etc/Zulu"),
|
|
||||||
("Europe/Amsterdam", "Europe/Amsterdam"),
|
|
||||||
("Europe/Andorra", "Europe/Andorra"),
|
|
||||||
("Europe/Astrakhan", "Europe/Astrakhan"),
|
|
||||||
("Europe/Athens", "Europe/Athens"),
|
|
||||||
("Europe/Belfast", "Europe/Belfast"),
|
|
||||||
("Europe/Belgrade", "Europe/Belgrade"),
|
|
||||||
("Europe/Berlin", "Europe/Berlin"),
|
|
||||||
("Europe/Bratislava", "Europe/Bratislava"),
|
|
||||||
("Europe/Brussels", "Europe/Brussels"),
|
|
||||||
("Europe/Bucharest", "Europe/Bucharest"),
|
|
||||||
("Europe/Budapest", "Europe/Budapest"),
|
|
||||||
("Europe/Busingen", "Europe/Busingen"),
|
|
||||||
("Europe/Chisinau", "Europe/Chisinau"),
|
|
||||||
("Europe/Copenhagen", "Europe/Copenhagen"),
|
|
||||||
("Europe/Dublin", "Europe/Dublin"),
|
|
||||||
("Europe/Gibraltar", "Europe/Gibraltar"),
|
|
||||||
("Europe/Guernsey", "Europe/Guernsey"),
|
|
||||||
("Europe/Helsinki", "Europe/Helsinki"),
|
|
||||||
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
|
||||||
("Europe/Istanbul", "Europe/Istanbul"),
|
|
||||||
("Europe/Jersey", "Europe/Jersey"),
|
|
||||||
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
|
||||||
("Europe/Kiev", "Europe/Kiev"),
|
|
||||||
("Europe/Kirov", "Europe/Kirov"),
|
|
||||||
("Europe/Lisbon", "Europe/Lisbon"),
|
|
||||||
("Europe/Ljubljana", "Europe/Ljubljana"),
|
|
||||||
("Europe/London", "Europe/London"),
|
|
||||||
("Europe/Luxembourg", "Europe/Luxembourg"),
|
|
||||||
("Europe/Madrid", "Europe/Madrid"),
|
|
||||||
("Europe/Malta", "Europe/Malta"),
|
|
||||||
("Europe/Mariehamn", "Europe/Mariehamn"),
|
|
||||||
("Europe/Minsk", "Europe/Minsk"),
|
|
||||||
("Europe/Monaco", "Europe/Monaco"),
|
|
||||||
("Europe/Moscow", "Europe/Moscow"),
|
|
||||||
("Europe/Nicosia", "Europe/Nicosia"),
|
|
||||||
("Europe/Oslo", "Europe/Oslo"),
|
|
||||||
("Europe/Paris", "Europe/Paris"),
|
|
||||||
("Europe/Podgorica", "Europe/Podgorica"),
|
|
||||||
("Europe/Prague", "Europe/Prague"),
|
|
||||||
("Europe/Riga", "Europe/Riga"),
|
|
||||||
("Europe/Rome", "Europe/Rome"),
|
|
||||||
("Europe/Samara", "Europe/Samara"),
|
|
||||||
("Europe/San_Marino", "Europe/San_Marino"),
|
|
||||||
("Europe/Sarajevo", "Europe/Sarajevo"),
|
|
||||||
("Europe/Saratov", "Europe/Saratov"),
|
|
||||||
("Europe/Simferopol", "Europe/Simferopol"),
|
|
||||||
("Europe/Skopje", "Europe/Skopje"),
|
|
||||||
("Europe/Sofia", "Europe/Sofia"),
|
|
||||||
("Europe/Stockholm", "Europe/Stockholm"),
|
|
||||||
("Europe/Tallinn", "Europe/Tallinn"),
|
|
||||||
("Europe/Tirane", "Europe/Tirane"),
|
|
||||||
("Europe/Tiraspol", "Europe/Tiraspol"),
|
|
||||||
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
|
||||||
("Europe/Uzhgorod", "Europe/Uzhgorod"),
|
|
||||||
("Europe/Vaduz", "Europe/Vaduz"),
|
|
||||||
("Europe/Vatican", "Europe/Vatican"),
|
|
||||||
("Europe/Vienna", "Europe/Vienna"),
|
|
||||||
("Europe/Vilnius", "Europe/Vilnius"),
|
|
||||||
("Europe/Volgograd", "Europe/Volgograd"),
|
|
||||||
("Europe/Warsaw", "Europe/Warsaw"),
|
|
||||||
("Europe/Zagreb", "Europe/Zagreb"),
|
|
||||||
("Europe/Zaporozhye", "Europe/Zaporozhye"),
|
|
||||||
("Europe/Zurich", "Europe/Zurich"),
|
|
||||||
("GB", "GB"),
|
|
||||||
("GB-Eire", "GB-Eire"),
|
|
||||||
("GMT", "GMT"),
|
|
||||||
("GMT+0", "GMT+0"),
|
|
||||||
("GMT-0", "GMT-0"),
|
|
||||||
("GMT0", "GMT0"),
|
|
||||||
("Greenwich", "Greenwich"),
|
|
||||||
("HST", "HST"),
|
|
||||||
("Hongkong", "Hongkong"),
|
|
||||||
("Iceland", "Iceland"),
|
|
||||||
("Indian/Antananarivo", "Indian/Antananarivo"),
|
|
||||||
("Indian/Chagos", "Indian/Chagos"),
|
|
||||||
("Indian/Christmas", "Indian/Christmas"),
|
|
||||||
("Indian/Cocos", "Indian/Cocos"),
|
|
||||||
("Indian/Comoro", "Indian/Comoro"),
|
|
||||||
("Indian/Kerguelen", "Indian/Kerguelen"),
|
|
||||||
("Indian/Mahe", "Indian/Mahe"),
|
|
||||||
("Indian/Maldives", "Indian/Maldives"),
|
|
||||||
("Indian/Mauritius", "Indian/Mauritius"),
|
|
||||||
("Indian/Mayotte", "Indian/Mayotte"),
|
|
||||||
("Indian/Reunion", "Indian/Reunion"),
|
|
||||||
("Iran", "Iran"),
|
|
||||||
("Israel", "Israel"),
|
|
||||||
("Jamaica", "Jamaica"),
|
|
||||||
("Japan", "Japan"),
|
|
||||||
("Kwajalein", "Kwajalein"),
|
|
||||||
("Libya", "Libya"),
|
|
||||||
("MET", "MET"),
|
|
||||||
("MST", "MST"),
|
|
||||||
("MST7MDT", "MST7MDT"),
|
|
||||||
("Mexico/BajaNorte", "Mexico/BajaNorte"),
|
|
||||||
("Mexico/BajaSur", "Mexico/BajaSur"),
|
|
||||||
("Mexico/General", "Mexico/General"),
|
|
||||||
("NZ", "NZ"),
|
|
||||||
("NZ-CHAT", "NZ-CHAT"),
|
|
||||||
("Navajo", "Navajo"),
|
|
||||||
("PRC", "PRC"),
|
|
||||||
("PST8PDT", "PST8PDT"),
|
|
||||||
("Pacific/Apia", "Pacific/Apia"),
|
|
||||||
("Pacific/Auckland", "Pacific/Auckland"),
|
|
||||||
("Pacific/Bougainville", "Pacific/Bougainville"),
|
|
||||||
("Pacific/Chatham", "Pacific/Chatham"),
|
|
||||||
("Pacific/Chuuk", "Pacific/Chuuk"),
|
|
||||||
("Pacific/Easter", "Pacific/Easter"),
|
|
||||||
("Pacific/Efate", "Pacific/Efate"),
|
|
||||||
("Pacific/Enderbury", "Pacific/Enderbury"),
|
|
||||||
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
|
||||||
("Pacific/Fiji", "Pacific/Fiji"),
|
|
||||||
("Pacific/Funafuti", "Pacific/Funafuti"),
|
|
||||||
("Pacific/Galapagos", "Pacific/Galapagos"),
|
|
||||||
("Pacific/Gambier", "Pacific/Gambier"),
|
|
||||||
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
|
||||||
("Pacific/Guam", "Pacific/Guam"),
|
|
||||||
("Pacific/Honolulu", "Pacific/Honolulu"),
|
|
||||||
("Pacific/Johnston", "Pacific/Johnston"),
|
|
||||||
("Pacific/Kanton", "Pacific/Kanton"),
|
|
||||||
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
|
||||||
("Pacific/Kosrae", "Pacific/Kosrae"),
|
|
||||||
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
|
||||||
("Pacific/Majuro", "Pacific/Majuro"),
|
|
||||||
("Pacific/Marquesas", "Pacific/Marquesas"),
|
|
||||||
("Pacific/Midway", "Pacific/Midway"),
|
|
||||||
("Pacific/Nauru", "Pacific/Nauru"),
|
|
||||||
("Pacific/Niue", "Pacific/Niue"),
|
|
||||||
("Pacific/Norfolk", "Pacific/Norfolk"),
|
|
||||||
("Pacific/Noumea", "Pacific/Noumea"),
|
|
||||||
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
|
||||||
("Pacific/Palau", "Pacific/Palau"),
|
|
||||||
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
|
||||||
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
|
||||||
("Pacific/Ponape", "Pacific/Ponape"),
|
|
||||||
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
|
||||||
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
|
||||||
("Pacific/Saipan", "Pacific/Saipan"),
|
|
||||||
("Pacific/Samoa", "Pacific/Samoa"),
|
|
||||||
("Pacific/Tahiti", "Pacific/Tahiti"),
|
|
||||||
("Pacific/Tarawa", "Pacific/Tarawa"),
|
|
||||||
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
|
||||||
("Pacific/Truk", "Pacific/Truk"),
|
|
||||||
("Pacific/Wake", "Pacific/Wake"),
|
|
||||||
("Pacific/Wallis", "Pacific/Wallis"),
|
|
||||||
("Pacific/Yap", "Pacific/Yap"),
|
|
||||||
("Poland", "Poland"),
|
|
||||||
("Portugal", "Portugal"),
|
|
||||||
("ROC", "ROC"),
|
|
||||||
("ROK", "ROK"),
|
|
||||||
("Singapore", "Singapore"),
|
|
||||||
("Turkey", "Turkey"),
|
|
||||||
("UCT", "UCT"),
|
|
||||||
("US/Alaska", "US/Alaska"),
|
|
||||||
("US/Aleutian", "US/Aleutian"),
|
|
||||||
("US/Arizona", "US/Arizona"),
|
|
||||||
("US/Central", "US/Central"),
|
|
||||||
("US/East-Indiana", "US/East-Indiana"),
|
|
||||||
("US/Eastern", "US/Eastern"),
|
|
||||||
("US/Hawaii", "US/Hawaii"),
|
|
||||||
("US/Indiana-Starke", "US/Indiana-Starke"),
|
|
||||||
("US/Michigan", "US/Michigan"),
|
|
||||||
("US/Mountain", "US/Mountain"),
|
|
||||||
("US/Pacific", "US/Pacific"),
|
|
||||||
("US/Samoa", "US/Samoa"),
|
|
||||||
("UTC", "UTC"),
|
|
||||||
("Universal", "Universal"),
|
|
||||||
("W-SU", "W-SU"),
|
|
||||||
("WET", "WET"),
|
|
||||||
("Zulu", "Zulu"),
|
|
||||||
],
|
|
||||||
default="UTC",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="notification",
|
|
||||||
constraint=models.CheckConstraint(
|
|
||||||
check=models.Q(
|
|
||||||
(
|
|
||||||
"notification_type__in",
|
|
||||||
[
|
|
||||||
"FAVORITE",
|
|
||||||
"REPLY",
|
|
||||||
"MENTION",
|
|
||||||
"TAG",
|
|
||||||
"FOLLOW",
|
|
||||||
"FOLLOW_REQUEST",
|
|
||||||
"BOOST",
|
|
||||||
"IMPORT",
|
|
||||||
"ADD",
|
|
||||||
"REPORT",
|
|
||||||
"INVITE",
|
|
||||||
"ACCEPT",
|
|
||||||
"JOIN",
|
|
||||||
"LEAVE",
|
|
||||||
"REMOVE",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
name="notification_type_valid",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="groupmemberinvitation",
|
|
||||||
name="group",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="user_invitations",
|
|
||||||
to="bookwyrm.group",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="groupmemberinvitation",
|
|
||||||
name="user",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="group_invitations",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="groupmember",
|
|
||||||
name="group",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="memberships",
|
|
||||||
to="bookwyrm.group",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="groupmember",
|
|
||||||
name="user",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="memberships",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="group",
|
|
||||||
name="user",
|
|
||||||
field=bookwyrm.models.fields.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="list",
|
|
||||||
name="group",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="bookwyrm.group",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="notification",
|
|
||||||
name="related_group",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="notifications",
|
|
||||||
to="bookwyrm.group",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="groupmemberinvitation",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("group", "user"), name="unique_invitation"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="groupmember",
|
|
||||||
constraint=models.UniqueConstraint(
|
|
||||||
fields=("group", "user"), name="unique_membership"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,31 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-11 17:38
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0107_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-15 15:54
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0108_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="status",
|
|
||||||
name="edited_date",
|
|
||||||
field=bookwyrm.models.fields.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-15 17:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0109_status_edited_date"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="quotation",
|
|
||||||
name="raw_quote",
|
|
||||||
field=models.TextField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="status",
|
|
||||||
name="raw_content",
|
|
||||||
field=models.TextField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-16 19:30
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0107_auto_20211016_0639"),
|
|
||||||
("bookwyrm", "0110_auto_20211015_1734"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = []
|
|
|
@ -1,93 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-10-22 08:44
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0111_merge_0107_auto_20211016_0639_0110_auto_20211015_1734"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveConstraint(
|
|
||||||
model_name="notification",
|
|
||||||
name="notification_type_valid",
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="notification",
|
|
||||||
name="notification_type",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("FAVORITE", "Favorite"),
|
|
||||||
("REPLY", "Reply"),
|
|
||||||
("MENTION", "Mention"),
|
|
||||||
("TAG", "Tag"),
|
|
||||||
("FOLLOW", "Follow"),
|
|
||||||
("FOLLOW_REQUEST", "Follow Request"),
|
|
||||||
("BOOST", "Boost"),
|
|
||||||
("IMPORT", "Import"),
|
|
||||||
("ADD", "Add"),
|
|
||||||
("REPORT", "Report"),
|
|
||||||
("INVITE", "Invite"),
|
|
||||||
("ACCEPT", "Accept"),
|
|
||||||
("JOIN", "Join"),
|
|
||||||
("LEAVE", "Leave"),
|
|
||||||
("REMOVE", "Remove"),
|
|
||||||
("GROUP_PRIVACY", "Group Privacy"),
|
|
||||||
("GROUP_NAME", "Group Name"),
|
|
||||||
("GROUP_DESCRIPTION", "Group Description"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name="notification",
|
|
||||||
constraint=models.CheckConstraint(
|
|
||||||
check=models.Q(
|
|
||||||
(
|
|
||||||
"notification_type__in",
|
|
||||||
[
|
|
||||||
"FAVORITE",
|
|
||||||
"REPLY",
|
|
||||||
"MENTION",
|
|
||||||
"TAG",
|
|
||||||
"FOLLOW",
|
|
||||||
"FOLLOW_REQUEST",
|
|
||||||
"BOOST",
|
|
||||||
"IMPORT",
|
|
||||||
"ADD",
|
|
||||||
"REPORT",
|
|
||||||
"INVITE",
|
|
||||||
"ACCEPT",
|
|
||||||
"JOIN",
|
|
||||||
"LEAVE",
|
|
||||||
"REMOVE",
|
|
||||||
"GROUP_PRIVACY",
|
|
||||||
"GROUP_NAME",
|
|
||||||
"GROUP_DESCRIPTION",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
name="notification_type_valid",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,25 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-10 21:04
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0112_auto_20211022_0844"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="importitem",
|
|
||||||
name="normalized_data",
|
|
||||||
field=models.JSONField(default={}),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="importjob",
|
|
||||||
name="mappings",
|
|
||||||
field=models.JSONField(default={}),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-13 00:56
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0113_auto_20211110_2104"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="importjob",
|
|
||||||
name="source",
|
|
||||||
field=models.CharField(default="Import", max_length=100),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-13 19:35
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0114_importjob_source"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="importitem",
|
|
||||||
name="linked_review",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="bookwyrm.review",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-14 17:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.utils.timezone
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0115_importitem_linked_review"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="importjob",
|
|
||||||
name="task_id",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="importjob",
|
|
||||||
name="updated_date",
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-15 18:22
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0116_auto_20211114_1734"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("lt-lt", "lietuvių (Lithuanian)"),
|
|
||||||
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-17 18:01
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0117_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("gl-es", "Galego (Galician)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
|
||||||
("pt-br", "Português - Brasil (Brazilian Portuguese)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-11-24 10:15
|
|
||||||
|
|
||||||
import bookwyrm.models.user
|
|
||||||
import django.contrib.postgres.fields
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0118_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="feed_status_types",
|
|
||||||
field=django.contrib.postgres.fields.ArrayField(
|
|
||||||
base_field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("review", "Reviews"),
|
|
||||||
("comment", "Comments"),
|
|
||||||
("quotation", "Quotations"),
|
|
||||||
("everything", "Everything else"),
|
|
||||||
],
|
|
||||||
max_length=10,
|
|
||||||
),
|
|
||||||
default=bookwyrm.models.user.get_feed_filter_choices,
|
|
||||||
size=8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,29 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-12-04 10:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
def gen_uuid(apps, schema_editor):
|
|
||||||
"""sets an unique UUID for embed_key"""
|
|
||||||
book_lists = apps.get_model("bookwyrm", "List")
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
for book_list in book_lists.objects.using(db_alias).all():
|
|
||||||
book_list.embed_key = uuid.uuid4()
|
|
||||||
book_list.save(broadcast=False)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0119_user_feed_status_types"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="list",
|
|
||||||
name="embed_key",
|
|
||||||
field=models.UUIDField(editable=False, null=True, unique=True),
|
|
||||||
),
|
|
||||||
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2021-12-22 11:06
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0120_list_embed_key"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="summary_keys",
|
|
||||||
field=models.JSONField(null=True),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2022-01-04 18:59
|
|
||||||
|
|
||||||
import bookwyrm.models.user
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0121_user_summary_keys"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="annualgoal",
|
|
||||||
name="year",
|
|
||||||
field=models.IntegerField(default=bookwyrm.models.user.get_current_year),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,34 +0,0 @@
|
||||||
# Generated by Django 3.2.5 on 2022-01-04 22:31
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0122_alter_annualgoal_year"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("gl-es", "Galego (Galician)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
|
||||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
|
||||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-06 17:59
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def get_admins(apps, schema_editor):
|
|
||||||
"""add any superusers to the "admin" group"""
|
|
||||||
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
groups = apps.get_model("auth", "Group")
|
|
||||||
try:
|
|
||||||
group = groups.objects.using(db_alias).get(name="admin")
|
|
||||||
except groups.DoesNotExist:
|
|
||||||
# for tests
|
|
||||||
return
|
|
||||||
|
|
||||||
users = apps.get_model("bookwyrm", "User")
|
|
||||||
admins = users.objects.using(db_alias).filter(is_superuser=True)
|
|
||||||
|
|
||||||
for admin in admins:
|
|
||||||
admin.groups.add(group)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0123_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(get_admins, reverse_code=migrations.RunPython.noop),
|
|
||||||
]
|
|
|
@ -1,36 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-09 01:06
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0124_auto_20220106_1759"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("gl-es", "Galego (Galician)"),
|
|
||||||
("it-it", "Italiano (Italian)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
|
||||||
("no-no", "Norsk (Norwegian)"),
|
|
||||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
|
||||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,55 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-12 23:15
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0125_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="annualgoal",
|
|
||||||
name="privacy",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("public", "Public"),
|
|
||||||
("unlisted", "Unlisted"),
|
|
||||||
("followers", "Followers"),
|
|
||||||
("direct", "Private"),
|
|
||||||
],
|
|
||||||
default="public",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="importjob",
|
|
||||||
name="privacy",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("public", "Public"),
|
|
||||||
("unlisted", "Unlisted"),
|
|
||||||
("followers", "Followers"),
|
|
||||||
("direct", "Private"),
|
|
||||||
],
|
|
||||||
default="public",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="default_post_privacy",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("public", "Public"),
|
|
||||||
("unlisted", "Unlisted"),
|
|
||||||
("followers", "Followers"),
|
|
||||||
("direct", "Private"),
|
|
||||||
],
|
|
||||||
default="public",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,144 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-10 21:20
|
|
||||||
|
|
||||||
import bookwyrm.models.activitypub_mixin
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0125_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="LinkDomain",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_date", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
|
||||||
"remote_id",
|
|
||||||
bookwyrm.models.fields.RemoteIdField(
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("domain", models.CharField(max_length=255, unique=True)),
|
|
||||||
(
|
|
||||||
"status",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("approved", "Approved"),
|
|
||||||
("blocked", "Blocked"),
|
|
||||||
("pending", "Pending"),
|
|
||||||
],
|
|
||||||
default="pending",
|
|
||||||
max_length=50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=100)),
|
|
||||||
(
|
|
||||||
"reported_by",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Link",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_date", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
|
||||||
"remote_id",
|
|
||||||
bookwyrm.models.fields.RemoteIdField(
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
validators=[bookwyrm.models.fields.validate_remote_id],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("url", bookwyrm.models.fields.URLField(max_length=255)),
|
|
||||||
(
|
|
||||||
"added_by",
|
|
||||||
bookwyrm.models.fields.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"domain",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="links",
|
|
||||||
to="bookwyrm.linkdomain",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="FileLink",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"link_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="bookwyrm.link",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("filetype", bookwyrm.models.fields.CharField(max_length=5)),
|
|
||||||
(
|
|
||||||
"book",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="file_links",
|
|
||||||
to="bookwyrm.book",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
bases=("bookwyrm.link",),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-10 22:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0126_filelink_link_linkdomain"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveConstraint(
|
|
||||||
model_name="report",
|
|
||||||
name="self_report",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="report",
|
|
||||||
name="links",
|
|
||||||
field=models.ManyToManyField(blank=True, to="bookwyrm.Link"),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-13 01:14
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0126_auto_20220112_2315"),
|
|
||||||
("bookwyrm", "0127_auto_20220110_2211"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = []
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-17 17:16
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0128_merge_0126_auto_20220112_2315_0127_auto_20220110_2211"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="filelink",
|
|
||||||
name="availability",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
choices=[
|
|
||||||
("free", "Free"),
|
|
||||||
("purchase", "Purchasable"),
|
|
||||||
("loan", "Available for loan"),
|
|
||||||
],
|
|
||||||
default="free",
|
|
||||||
max_length=100,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="filelink",
|
|
||||||
name="filetype",
|
|
||||||
field=bookwyrm.models.fields.CharField(max_length=50),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-24 20:01
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0129_auto_20220117_1716"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="listitem",
|
|
||||||
name="notes",
|
|
||||||
field=bookwyrm.models.fields.TextField(
|
|
||||||
blank=True, max_length=300, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-24 17:32
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0129_auto_20220117_1716"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("gl-es", "Galego (Galician)"),
|
|
||||||
("it-it", "Italiano (Italian)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
|
||||||
("no-no", "Norsk (Norwegian)"),
|
|
||||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
|
||||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
|
||||||
("sv-se", "Swedish (Svenska)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-01-25 16:44
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0130_alter_listitem_notes"),
|
|
||||||
("bookwyrm", "0130_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = []
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Generated by Django 3.2.10 on 2022-02-02 20:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0131_merge_20220125_1644"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="preferred_language",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("en-us", "English"),
|
|
||||||
("de-de", "Deutsch (German)"),
|
|
||||||
("es-es", "Español (Spanish)"),
|
|
||||||
("gl-es", "Galego (Galician)"),
|
|
||||||
("it-it", "Italiano (Italian)"),
|
|
||||||
("fr-fr", "Français (French)"),
|
|
||||||
("lt-lt", "Lietuvių (Lithuanian)"),
|
|
||||||
("no-no", "Norsk (Norwegian)"),
|
|
||||||
("pt-br", "Português do Brasil (Brazilian Portuguese)"),
|
|
||||||
("pt-pt", "Português Europeu (European Portuguese)"),
|
|
||||||
("sv-se", "Svenska (Swedish)"),
|
|
||||||
("zh-hans", "简体中文 (Simplified Chinese)"),
|
|
||||||
("zh-hant", "繁體中文 (Traditional Chinese)"),
|
|
||||||
],
|
|
||||||
max_length=255,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Generated by Django 3.2.11 on 2022-02-04 20:06
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0132_alter_user_preferred_language"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="listitem",
|
|
||||||
name="notes",
|
|
||||||
field=bookwyrm.models.fields.HtmlField(
|
|
||||||
blank=True, max_length=300, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,29 +0,0 @@
|
||||||
# Generated by Django 3.2.11 on 2022-02-11 18:59
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0133_alter_listitem_notes"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="announcement",
|
|
||||||
name="display_type",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("white-ter", "None"),
|
|
||||||
("primary-light", "Primary"),
|
|
||||||
("success-light", "Success"),
|
|
||||||
("link-light", "Link"),
|
|
||||||
("warning-light", "Warning"),
|
|
||||||
("danger-light", "Danger"),
|
|
||||||
],
|
|
||||||
default="white-ter",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,58 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-17 16:24
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0134_announcement_display_type"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="author",
|
|
||||||
old_name="viaf_id",
|
|
||||||
new_name="viaf",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="edition",
|
|
||||||
name="asin",
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="author",
|
|
||||||
name="asin",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
blank=True, max_length=255, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="author",
|
|
||||||
name="wikidata",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
blank=True, max_length=255, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="book",
|
|
||||||
name="asin",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
blank=True, max_length=255, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="book",
|
|
||||||
name="viaf",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
blank=True, max_length=255, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="book",
|
|
||||||
name="wikidata",
|
|
||||||
field=bookwyrm.models.fields.CharField(
|
|
||||||
blank=True, max_length=255, null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-17 17:08
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0135_auto_20220217_1624"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="sitesettings",
|
|
||||||
name="admin_code",
|
|
||||||
field=models.CharField(default=uuid.uuid4, max_length=50),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="sitesettings",
|
|
||||||
name="install_mode",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-17 19:26
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0136_auto_20220217_1708"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="sitesettings",
|
|
||||||
name="allow_registration",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-24 18:59
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0137_alter_sitesettings_allow_registration"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="AutoMod",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("string_match", models.CharField(max_length=200, unique=True)),
|
|
||||||
("flag_users", models.BooleanField(default=True)),
|
|
||||||
("flag_statuses", models.BooleanField(default=True)),
|
|
||||||
(
|
|
||||||
"created_by",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,45 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-24 20:41
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
def set_report_statuses(apps, schema_editor):
|
|
||||||
"""copy over status fields"""
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
report_model = apps.get_model("bookwyrm", "Report")
|
|
||||||
reports = report_model.objects.using(db_alias).filter(statuses__isnull=False)
|
|
||||||
for report in reports:
|
|
||||||
report.status = report.statuses.first()
|
|
||||||
report.save()
|
|
||||||
|
|
||||||
|
|
||||||
def set_reverse(apps, schema_editor):
|
|
||||||
"""copy over status fields"""
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
report_model = apps.get_model("bookwyrm", "Report")
|
|
||||||
reports = report_model.objects.using(db_alias).filter(status__isnull=False)
|
|
||||||
for report in reports:
|
|
||||||
report.statuses.set(report.status)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0138_automod"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="report",
|
|
||||||
name="status",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
related_name="reports",
|
|
||||||
to="bookwyrm.status",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(set_report_statuses, reverse_code=set_reverse),
|
|
||||||
]
|
|
|
@ -1,17 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-24 20:43
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0139_report_status"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="report",
|
|
||||||
name="statuses",
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-24 20:50
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0140_remove_report_statuses"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="report",
|
|
||||||
name="status",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to="bookwyrm.status",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,68 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-27 17:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
def add_default_themes(apps, schema_editor):
|
|
||||||
"""add light and dark themes"""
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
theme_model = apps.get_model("bookwyrm", "Theme")
|
|
||||||
theme_model.objects.using(db_alias).create(
|
|
||||||
name="BookWyrm Light",
|
|
||||||
path="css/themes/bookwyrm-light.scss",
|
|
||||||
)
|
|
||||||
theme_model.objects.using(db_alias).create(
|
|
||||||
name="BookWyrm Dark",
|
|
||||||
path="css/themes/bookwyrm-dark.scss",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0141_alter_report_status"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Theme",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.AutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_date", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("name", models.CharField(max_length=50, unique=True)),
|
|
||||||
("path", models.CharField(max_length=50, unique=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="sitesettings",
|
|
||||||
name="default_theme",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="bookwyrm.theme",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="theme",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
to="bookwyrm.theme",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
add_default_themes, reverse_code=migrations.RunPython.noop
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-28 19:44
|
|
||||||
|
|
||||||
import bookwyrm.models.fields
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0141_alter_report_status"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="user",
|
|
||||||
name="hide_follows",
|
|
||||||
field=bookwyrm.models.fields.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-02-28 21:28
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0142_auto_20220227_1752"),
|
|
||||||
("bookwyrm", "0142_user_hide_follows"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = []
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2022-03-01 18:46
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def remove_white(apps, schema_editor):
|
|
||||||
"""don't hardcode white announcements"""
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
announcement_model = apps.get_model("bookwyrm", "Announcement")
|
|
||||||
announcement_model.objects.using(db_alias).filter(display_type="white-ter").update(
|
|
||||||
display_type=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookwyrm", "0143_merge_0142_auto_20220227_1752_0142_user_hide_follows"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="announcement",
|
|
||||||
name="display_type",
|
|
||||||
field=models.CharField(
|
|
||||||
blank=True,
|
|
||||||
choices=[
|
|
||||||
("primary-light", "Primary"),
|
|
||||||
("success-light", "Success"),
|
|
||||||
("link-light", "Link"),
|
|
||||||
("warning-light", "Warning"),
|
|
||||||
("danger-light", "Danger"),
|
|
||||||
],
|
|
||||||
max_length=20,
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(remove_white, reverse_code=migrations.RunPython.noop),
|
|
||||||
]
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue