Compare commits
469 Commits
nightly
...
versions/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c072a24890 | ||
|
|
fcfb705fb3 | ||
|
|
37c57056cd | ||
|
|
c721413209 | ||
|
|
07ea45992b | ||
|
|
9708131712 | ||
|
|
cdd0380f1d | ||
|
|
1eb9975dd6 | ||
|
|
4d8dc8e552 | ||
|
|
97fb5f96a7 | ||
|
|
d4403daa61 | ||
|
|
ff6e4294e9 | ||
|
|
eb6f88dfd1 | ||
|
|
83a4368eef | ||
|
|
b6e0de01f3 | ||
|
|
bda4902bc7 | ||
|
|
5de6fbe914 | ||
|
|
3bbef4253a | ||
|
|
6dd61f187f | ||
|
|
d55e9c0944 | ||
|
|
862c3ff568 | ||
|
|
a815c3f538 | ||
|
|
54539c9d03 | ||
|
|
2fbe4625c0 | ||
|
|
62c92ba6fd | ||
|
|
7aa4b480d7 | ||
|
|
490bbee81e | ||
|
|
5850ea99d4 | ||
|
|
952380502b | ||
|
|
97c9cfda6a | ||
|
|
c152156a11 | ||
|
|
0c312b343e | ||
|
|
7e141c1d04 | ||
|
|
50d4aa0e22 | ||
|
|
bf733be4c5 | ||
|
|
da2ff0bdd8 | ||
|
|
4b444a75cc | ||
|
|
378511aea3 | ||
|
|
2a1e060907 | ||
|
|
2cfd4a9095 | ||
|
|
711a28dccf | ||
|
|
b99cf97558 | ||
|
|
442faca915 | ||
|
|
0f84b7a723 | ||
|
|
2a67cf271e | ||
|
|
0cbd9e0d45 | ||
|
|
48aad4f356 | ||
|
|
5c5979c5af | ||
|
|
54100f7538 | ||
|
|
8599d69d23 | ||
|
|
23d56c3147 | ||
|
|
7e1de2623c | ||
|
|
21da6742b0 | ||
|
|
d546967d1d | ||
|
|
3917ca667a | ||
|
|
5c20a92f27 | ||
|
|
a91bc6716d | ||
|
|
3fc463bb1c | ||
|
|
2654c96e1c | ||
|
|
744d252640 | ||
|
|
d74d13450c | ||
|
|
b975c75c2f | ||
|
|
1cb3f9fe60 | ||
|
|
68c67abaa3 | ||
|
|
ef415ef826 | ||
|
|
5a8e691388 | ||
|
|
5f264e2aae | ||
|
|
0e524e44ed | ||
|
|
023d82c96c | ||
|
|
9370b2effb | ||
|
|
30e8327db9 | ||
|
|
7eaa096ad7 | ||
|
|
495cd18e34 | ||
|
|
8c3e4fa5c0 | ||
|
|
c2dd01d51e | ||
|
|
e03f017e7f | ||
|
|
0b8b3c31d2 | ||
|
|
931d31cf02 | ||
|
|
5d149c5968 | ||
|
|
d200f6d3c9 | ||
|
|
5ef12555a4 | ||
|
|
11252ac397 | ||
|
|
1c3595c66e | ||
|
|
bb7bbb299b | ||
|
|
070df8ae37 | ||
|
|
95faa44d76 | ||
|
|
cae7b8f8c5 | ||
|
|
bb6a827f28 | ||
|
|
863a2680a9 | ||
|
|
efde174b1a | ||
|
|
6eb986f7d1 | ||
|
|
d85e838480 | ||
|
|
b546be8ea2 | ||
|
|
ba17fe742a | ||
|
|
5d716cd69d | ||
|
|
529ab2a6ad | ||
|
|
b25c3be969 | ||
|
|
18e5ee1e4f | ||
|
|
6a57a5a7de | ||
|
|
8589004173 | ||
|
|
23b1375289 | ||
|
|
fb608bba98 | ||
|
|
d28bb60abd | ||
|
|
f3f7b4bb7d | ||
|
|
1fee7260e4 | ||
|
|
dcd1af685a | ||
|
|
b633238610 | ||
|
|
ae1634c378 | ||
|
|
61ebda6e63 | ||
|
|
e4af406d5f | ||
|
|
ee2637dddc | ||
|
|
7d3677acfb | ||
|
|
999e164c3d | ||
|
|
278f97b7e4 | ||
|
|
ea3ba2c4de | ||
|
|
627056f1ae | ||
|
|
7ba47d5c5f | ||
|
|
27517c04f2 | ||
|
|
e9cdc958f6 | ||
|
|
8284dcf306 | ||
|
|
a4ef6b3e8a | ||
|
|
71c2a7773e | ||
|
|
bd12d587ee | ||
|
|
4ab2b4fee0 | ||
|
|
67cd01f5ae | ||
|
|
f93ae2f395 | ||
|
|
0918931713 | ||
|
|
aa95a61451 | ||
|
|
dcea32ae38 | ||
|
|
4376d76c8a | ||
|
|
5b6a6bccb2 | ||
|
|
991bd9df32 | ||
|
|
6143cb5155 | ||
|
|
e5cd5a40c3 | ||
|
|
f92d99bd9a | ||
|
|
8e66eefe7c | ||
|
|
125a4317f4 | ||
|
|
0a864c2f07 | ||
|
|
0919718114 | ||
|
|
9328a3e26e | ||
|
|
cce6636b05 | ||
|
|
d5fc50272d | ||
|
|
e97dde5b46 | ||
|
|
495ac8d196 | ||
|
|
3c2d2d1087 | ||
|
|
e007af6b3f | ||
|
|
3976766abe | ||
|
|
43d79a9d86 | ||
|
|
66670a5d59 | ||
|
|
a06c633568 | ||
|
|
c61f709c1b | ||
|
|
38c4643302 | ||
|
|
65d75dafde | ||
|
|
46812ab3d3 | ||
|
|
4ba2d375af | ||
|
|
b4a81ee0bc | ||
|
|
08fac9fd9d | ||
|
|
5bab080553 | ||
|
|
f65f363361 | ||
|
|
3bd33db023 | ||
|
|
b4188de727 | ||
|
|
fcfe7686fa | ||
|
|
a64bc61810 | ||
|
|
da638dc7f9 | ||
|
|
b5839c0662 | ||
|
|
ef5e0c2d86 | ||
|
|
fbb3a64bce | ||
|
|
c09b58894b | ||
|
|
eae5359cdf | ||
|
|
27546dadd9 | ||
|
|
74dfa53787 | ||
|
|
7532429b0b | ||
|
|
9261b6e687 | ||
|
|
33e0e694e3 | ||
|
|
4937d8b776 | ||
|
|
890f872681 | ||
|
|
9ce930367d | ||
|
|
746f40dda0 | ||
|
|
319b74c85f | ||
|
|
2ed7858acb | ||
|
|
c5d4054fb6 | ||
|
|
382995ae40 | ||
|
|
f076a49d2d | ||
|
|
c5ce20bbea | ||
|
|
9203977261 | ||
|
|
daf79983aa | ||
|
|
75fd7647d4 | ||
|
|
8c085331f1 | ||
|
|
3f48a5549e | ||
|
|
c059f1f021 | ||
|
|
1d0ebbab64 | ||
|
|
a769cc92e3 | ||
|
|
205ca594f5 | ||
|
|
108c54630f | ||
|
|
c7bd2ee8f2 | ||
|
|
55356c4781 | ||
|
|
83a9b5a60a | ||
|
|
50333d1326 | ||
|
|
ad7c77b4f3 | ||
|
|
166183dff9 | ||
|
|
09edab5027 | ||
|
|
027a853885 | ||
|
|
2e5d05403a | ||
|
|
c0b34067ef | ||
|
|
fc29309f68 | ||
|
|
14fd5f02a8 | ||
|
|
622972fd85 | ||
|
|
6376445cc4 | ||
|
|
53f3261dae | ||
|
|
79742e82f9 | ||
|
|
3f97bc1a68 | ||
|
|
a15f0b7641 | ||
|
|
383d0fcc38 | ||
|
|
7c4ae1aef0 | ||
|
|
16d8fb9fea | ||
|
|
9ed93b54af | ||
|
|
2d9aca55c5 | ||
|
|
354ea434ae | ||
|
|
5d7f810477 | ||
|
|
b53c026877 | ||
|
|
097ac7dae6 | ||
|
|
0e800dc314 | ||
|
|
38d7b7cda3 | ||
|
|
9d8c8f4833 | ||
|
|
da1d32f6cd | ||
|
|
8f24b2ed80 | ||
|
|
58e38c1ff9 | ||
|
|
65ccbd3b7b | ||
|
|
c6aab93f98 | ||
|
|
b0e3b82755 | ||
|
|
3dc8df46b9 | ||
|
|
cdb29b11f9 | ||
|
|
924538fe48 | ||
|
|
125c133334 | ||
|
|
92e615ce4c | ||
|
|
3e53ce0c43 | ||
|
|
39689e2a4f | ||
|
|
57d0bba0fa | ||
|
|
c332fa4538 | ||
|
|
a77528862f | ||
|
|
b3a781d51a | ||
|
|
da4e4d0b46 | ||
|
|
dfd548bf62 | ||
|
|
86b0463a38 | ||
|
|
8e0a2bbdbc | ||
|
|
2cbac826d4 | ||
|
|
7f3b28aec8 | ||
|
|
35ef8ba7b8 | ||
|
|
ccd935d752 | ||
|
|
5365ed4fed | ||
|
|
5cb674b7ab | ||
|
|
ffeb580c15 | ||
|
|
45ceab024d | ||
|
|
cd9d51db9e | ||
|
|
82651ff32c | ||
|
|
23a4a56aae | ||
|
|
c40e0c136a | ||
|
|
000fe87c37 | ||
|
|
442bf5dc4b | ||
|
|
f96057b0fd | ||
|
|
e687430cf0 | ||
|
|
24066c494e | ||
|
|
945eed7ad5 | ||
|
|
0ee82e9efe | ||
|
|
8d3f26bd7f | ||
|
|
eeceb52c06 | ||
|
|
57269ca7f9 | ||
|
|
c9ce90ea31 | ||
|
|
50ea0c15df | ||
|
|
5a90d76005 | ||
|
|
43691de6b7 | ||
|
|
fe2cf70d93 | ||
|
|
49c9fbbce1 | ||
|
|
4ff9794286 | ||
|
|
d6c7a0d765 | ||
|
|
5e4cbbe2bc | ||
|
|
9e4ebf4e04 | ||
|
|
bd194a70cb | ||
|
|
6dcd8bd9aa | ||
|
|
2c3e2e2bef | ||
|
|
2402668e16 | ||
|
|
38228b4fe8 | ||
|
|
91465ef9b0 | ||
|
|
ff24e17eb6 | ||
|
|
d687e62106 | ||
|
|
15bec5fcdb | ||
|
|
9be8f02829 | ||
|
|
fd7e937cef | ||
|
|
931b17a447 | ||
|
|
32fb40548a | ||
|
|
b848737515 | ||
|
|
4a0e9ffa15 | ||
|
|
ae22e0f70c | ||
|
|
012c027994 | ||
|
|
c1c8d1dc2d | ||
|
|
25edc73100 | ||
|
|
36101dfea6 | ||
|
|
28cc228b5a | ||
|
|
64e1c6bb67 | ||
|
|
360e756093 | ||
|
|
b79c168dab | ||
|
|
14f31d5614 | ||
|
|
9784798118 | ||
|
|
8ca6c563bc | ||
|
|
213f3c1fb4 | ||
|
|
5623f0b3a4 | ||
|
|
79cba7abe1 | ||
|
|
9bcaf1849b | ||
|
|
c2fc10c344 | ||
|
|
263d646c7c | ||
|
|
bd54877e0c | ||
|
|
60ac63ead4 | ||
|
|
f77f64cc71 | ||
|
|
7672aca7a9 | ||
|
|
76853147c8 | ||
|
|
f71ca8f2f5 | ||
|
|
ba48a7e0fd | ||
|
|
798446f362 | ||
|
|
727d2ecd71 | ||
|
|
3a6a250d1b | ||
|
|
6ed18926cc | ||
|
|
c2f10fd38d | ||
|
|
1d1600c5dd | ||
|
|
b83ab1b528 | ||
|
|
8559565dca | ||
|
|
77468a87be | ||
|
|
0e86f2ad8a | ||
|
|
8c63ef4c69 | ||
|
|
3adb9d4ea0 | ||
|
|
4d46ca3343 | ||
|
|
315e70309b | ||
|
|
34443a715c | ||
|
|
46c2192d9a | ||
|
|
108744cdd5 | ||
|
|
15180e95bf | ||
|
|
503af584d5 | ||
|
|
b28281be5a | ||
|
|
feb92a105f | ||
|
|
0a7908baca | ||
|
|
1efec6bd41 | ||
|
|
68995adb7f | ||
|
|
2b52ee11b2 | ||
|
|
5d944b922f | ||
|
|
ae2205fe30 | ||
|
|
84e78f16d9 | ||
|
|
e83f6e55a0 | ||
|
|
bc816ccdda | ||
|
|
c7dec2ee09 | ||
|
|
f8c25af796 | ||
|
|
e048f31f85 | ||
|
|
278fdc3c9a | ||
|
|
6832450221 | ||
|
|
e699e39c37 | ||
|
|
55e9b2263c | ||
|
|
034e0668f4 | ||
|
|
da6d7cbc0c | ||
|
|
2f3d640799 | ||
|
|
fad2ae3683 | ||
|
|
aaea84b386 | ||
|
|
0c7f9f50af | ||
|
|
f744eb8871 | ||
|
|
3ae991c9cd | ||
|
|
60233e0b89 | ||
|
|
50e72fdb4e | ||
|
|
255b1c75ea | ||
|
|
ab1482152e | ||
|
|
adab93fad6 | ||
|
|
99a1d143ee | ||
|
|
28f387cf6c | ||
|
|
8039dfa30a | ||
|
|
28a1ecb685 | ||
|
|
3ab41e6b63 | ||
|
|
46cd7353dc | ||
|
|
4d84b5f28f | ||
|
|
0d9bda0ccf | ||
|
|
dc255da362 | ||
|
|
ef126d56b2 | ||
|
|
d6435b7735 | ||
|
|
d1a4cb875b | ||
|
|
8d8ec59e03 | ||
|
|
55a30379bd | ||
|
|
8740707d1d | ||
|
|
27c04ed9be | ||
|
|
fd4c2e7f00 | ||
|
|
685d9b6d3e | ||
|
|
c430d471e6 | ||
|
|
8aeb7f01fe | ||
|
|
d1945b6190 | ||
|
|
c466c44dfb | ||
|
|
55cd928069 | ||
|
|
f6a675c9db | ||
|
|
2d4a710999 | ||
|
|
f9dfc00b30 | ||
|
|
2379f6963f | ||
|
|
59fbbd82e2 | ||
|
|
8e69178e07 | ||
|
|
146ebb7032 | ||
|
|
1515c0170f | ||
|
|
984a1903ce | ||
|
|
f4e0e06c66 | ||
|
|
66b04296f5 | ||
|
|
d6394c5e3b | ||
|
|
396f9f6fca | ||
|
|
1d1b4f5f5f | ||
|
|
89d3fb9922 | ||
|
|
77fc9b5831 | ||
|
|
a414b8df92 | ||
|
|
f25174bd15 | ||
|
|
7a4d230195 | ||
|
|
d210f05aa6 | ||
|
|
8bf484051e | ||
|
|
ab045c499c | ||
|
|
abfc8b0c09 | ||
|
|
52bbf62e26 | ||
|
|
e0d900d952 | ||
|
|
cfe1934b9b | ||
|
|
ef3453b48c | ||
|
|
2bd649ab52 | ||
|
|
13524f5ce0 | ||
|
|
c6104e0080 | ||
|
|
f9a0fb2e79 | ||
|
|
d973a20c30 | ||
|
|
b8b10592c7 | ||
|
|
96ee283226 | ||
|
|
0188737e74 | ||
|
|
46bd5b0a17 | ||
|
|
10ccab662f | ||
|
|
77dc53f244 | ||
|
|
21d7ec2428 | ||
|
|
2ca38c20b0 | ||
|
|
67e79d0e19 | ||
|
|
141d79afa6 | ||
|
|
e9411514c7 | ||
|
|
2f70a57f18 | ||
|
|
5a626861ae | ||
|
|
aaf9f7a8be | ||
|
|
0f5625a356 | ||
|
|
cb408c768d | ||
|
|
03d51fe8e1 | ||
|
|
3394f97b25 | ||
|
|
7c6d466ab1 | ||
|
|
ba4858e77f | ||
|
|
544edea54a | ||
|
|
2738e3facf | ||
|
|
51f15a3131 | ||
|
|
5c1eb59a1a | ||
|
|
f5f6470697 | ||
|
|
d5224d93a7 | ||
|
|
2047fb7b17 | ||
|
|
4b727662ef | ||
|
|
6a7cd09bc1 | ||
|
|
42d434f7bb | ||
|
|
e72b4e82a3 | ||
|
|
8e896a54f9 | ||
|
|
2d5a646940 | ||
|
|
b3c0b622b8 | ||
|
|
b04b205fb6 | ||
|
|
8c98679687 | ||
|
|
6d39f3b716 | ||
|
|
21a6ed4756 | ||
|
|
3b247bfb5f | ||
|
|
823083a76c | ||
|
|
4ee6add201 | ||
|
|
894a25ccce | ||
|
|
ecdc4a9a51 | ||
|
|
2ae56d2cf4 | ||
|
|
810558659d | ||
|
|
957cf64fe5 | ||
|
|
e8c70cbd08 |
6
.isort.cfg
Normal file
@@ -0,0 +1,6 @@
|
||||
[settings]
|
||||
default_section = THIRDPARTY
|
||||
known_first_party = mayan
|
||||
known_django = django
|
||||
multi_line_output = 5
|
||||
sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||
18
.tx/config
@@ -19,6 +19,12 @@ source_lang = en
|
||||
source_file = mayan/apps/authentication/locale/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
[mayan-edms.autoadmin-2-0]
|
||||
file_filter = mayan/apps/autoadmin/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
source_file = mayan/apps/autoadmin/locale/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
[mayan-edms.cabinets-2-0]
|
||||
file_filter = mayan/apps/cabinets/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
@@ -43,6 +49,12 @@ source_lang = en
|
||||
source_file = mayan/apps/converter/locale/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
[mayan-edms.dashboards-2-0]
|
||||
file_filter = mayan/apps/dashboards/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
source_file = mayan/apps/dashboards/locale/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
[mayan-edms.django_gpg-2-0]
|
||||
file_filter = mayan/apps/django_gpg/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
@@ -97,6 +109,12 @@ source_lang = en
|
||||
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
[mayan-edms.file_caching-2-0]
|
||||
file_filter = mayan/apps/file_caching/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
source_file = mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po
|
||||
type = PO
|
||||
|
||||
[mayan-edms.linking-2-0]
|
||||
file_filter = mayan/apps/linking/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
|
||||
435
HISTORY.rst
7
Makefile
@@ -62,15 +62,16 @@ clean-pyc:
|
||||
find . -name '*.pyc' -exec rm -f {} +
|
||||
find . -name '*.pyo' -exec rm -f {} +
|
||||
find . -name '*~' -exec rm -f {} +
|
||||
find . -name '__pycache__' -exec rm -R -f {} +
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
test:
|
||||
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations
|
||||
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
|
||||
|
||||
test-all:
|
||||
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations
|
||||
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
|
||||
|
||||
test-launch-postgres:
|
||||
@docker rm -f test-postgres || true
|
||||
@@ -283,7 +284,7 @@ test-with-docker-frontend:
|
||||
./manage.py runserver --settings=mayan.settings.staging.docker
|
||||
|
||||
test-with-docker-worker:
|
||||
./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO -O fair
|
||||
./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO
|
||||
|
||||
docker-mysql-on:
|
||||
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql
|
||||
|
||||
@@ -18,7 +18,7 @@ sudo apt-get -qq update
|
||||
sudo apt-get -y upgrade
|
||||
|
||||
echo -e "\n -> Installing core binaries \n"
|
||||
sudo apt-get -y install git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice
|
||||
sudo apt-get -y install exiftool git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice
|
||||
|
||||
echo -e "\n -> Cloning development branch of repository \n"
|
||||
git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY
|
||||
|
||||
@@ -1445,15 +1445,15 @@ sudo -u mayan \
|
||||
dialog --infobox "Preparing static files" 3 70
|
||||
sudo -u mayan \
|
||||
MAYAN_MEDIA_ROOT=$MAYAN_MEDIA_ROOT \
|
||||
$MAYAN_BIN collectstatic --noinput > /dev/null
|
||||
$MAYAN_BIN preparestatic --noinput > /dev/null
|
||||
|
||||
# Create supervisor file for gunicorn (frontend), 3 background workers, and the scheduler for periodic tasks
|
||||
cat > /etc/supervisor/conf.d/mayan.conf <<EOF
|
||||
[supervisord]
|
||||
environment=
|
||||
MAYAN_ALLOWED_HOSTS="*", # Allow access to other network hosts other than localhost
|
||||
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||
MAYAN_CELERY_RESULT_BACKEND="redis://127.0.0.1:6379/0",
|
||||
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||
PYTHONPATH=${MAYAN_INSTALLATION_FOLDER}/lib/python2.7/site-packages:$MAYAN_MEDIA_ROOT,
|
||||
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql,
|
||||
MAYAN_DATABASE_HOST=127.0.0.1,
|
||||
@@ -1473,7 +1473,7 @@ user = mayan
|
||||
[program:mayan-worker-fast]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 1 ${MAYAN_BIN} celery worker -Ofair -l ERROR -Q converter -n mayan-worker-fast.%%h --concurrency=1
|
||||
command = nice -n 1 ${MAYAN_BIN} celery worker -l ERROR -Q converter -n mayan-worker-fast.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -1484,7 +1484,7 @@ user = mayan
|
||||
[program:mayan-worker-medium]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 18 ${MAYAN_BIN} celery worker -Ofair -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h --concurrency=1
|
||||
command = nice -n 18 ${MAYAN_BIN} celery worker -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -1495,7 +1495,7 @@ user = mayan
|
||||
[program:mayan-worker-slow]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 19 ${MAYAN_BIN} celery worker -Ofair -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h --concurrency=1
|
||||
command = nice -n 19 ${MAYAN_BIN} celery worker -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
|
||||
@@ -20,7 +20,7 @@ apt-get -qq update
|
||||
apt-get -y upgrade
|
||||
|
||||
echo -e "\n -> Installing core binaries \n"
|
||||
apt-get install nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y
|
||||
apt-get install exiftool nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y
|
||||
|
||||
echo -e "\n -> Setting up virtualenv \n"
|
||||
rm -f ${INSTALLATION_DIRECTORY}
|
||||
@@ -133,7 +133,7 @@ EOF
|
||||
echo -e "\n -> Creating the supervisor file for the Celery worker, /etc/supervisor/conf.d/mayan-celery.conf \n"
|
||||
cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF
|
||||
[program:mayan-worker]
|
||||
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -Ofair -l ERROR
|
||||
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -l ERROR
|
||||
directory = ${INSTALLATION_DIRECTORY}
|
||||
user = www-data
|
||||
stdout_logfile = /var/log/mayan/worker-stdout.log
|
||||
@@ -161,7 +161,7 @@ priority = 998
|
||||
EOF
|
||||
|
||||
echo -e "\n -> Collecting the static files \n"
|
||||
mayan-edms.py collectstatic --noinput
|
||||
mayan-edms.py preparestatic --noinput
|
||||
|
||||
echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
|
||||
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R
|
||||
|
||||
@@ -9,13 +9,14 @@ import sh
|
||||
|
||||
|
||||
APP_LIST = (
|
||||
'acls', 'appearance', 'authentication', 'cabinets', 'checkouts', 'common',
|
||||
'converter', 'django_gpg', 'document_comments', 'document_indexing',
|
||||
'document_parsing', 'document_signatures', 'document_states', 'documents',
|
||||
'dynamic_search', 'events', 'linking', 'lock_manager', 'mayan_statistics',
|
||||
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr', 'permissions',
|
||||
'rest_api', 'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
|
||||
'user_management'
|
||||
'acls', 'appearance', 'authentication', 'autoadmin', 'cabinets',
|
||||
'checkouts', 'common', 'converter', 'dashboards', 'django_gpg',
|
||||
'document_comments', 'document_indexing', 'document_parsing',
|
||||
'document_signatures', 'document_states', 'documents', 'dynamic_search',
|
||||
'events', 'file_caching', 'linking', 'lock_manager', 'mayan_statistics',
|
||||
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr',
|
||||
'permissions', 'rest_api', 'smart_settings', 'sources', 'storage',
|
||||
'tags', 'task_manager', 'user_management'
|
||||
)
|
||||
|
||||
LANGUAGE_LIST = (
|
||||
|
||||
@@ -21,6 +21,7 @@ RUN if [ "${APT_PROXY}" ]; then echo "Acquire::http { Proxy \"http://${APT_PROXY
|
||||
# Install base Ubuntu libraries
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
exiftool \
|
||||
g++ \
|
||||
gcc \
|
||||
ghostscript \
|
||||
@@ -131,11 +132,11 @@ COPY --from=BUILDER_IMAGE /code/docker/version .
|
||||
RUN chown -R mayan:mayan $PROJECT_INSTALL_DIR
|
||||
|
||||
# Install build Mayan EDMS
|
||||
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir *.whl && \
|
||||
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir --no-use-pep517 *.whl && \
|
||||
rm *.whl
|
||||
|
||||
# Install Python clients for librabbitmq, MySQL, PostgreSQL, REDIS
|
||||
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir librabbitmq==1.6.1 mysql-python==1.2.5 psycopg2==2.7.3.2 redis==2.10.6
|
||||
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir --no-use-pep517 librabbitmq==1.6.1 mysql-python==1.2.5 psycopg2==2.7.3.2 redis==2.10.6
|
||||
|
||||
# Setup supervisor
|
||||
COPY docker/etc/supervisor/mayan.conf /etc/supervisor/conf.d
|
||||
|
||||
@@ -126,13 +126,13 @@ Defaults to `None`. This optional environment variable is used to set the hostna
|
||||
|
||||
Defaults to `None`. This optional environment variable is used to set the port number to use when connecting to the database. An empty string means the default port. Not used with SQLite. For more information read the pertinent Django documentation page: [Settings, PORT](https://docs.djangoproject.com/en/1.11/ref/settings/#port)
|
||||
|
||||
### `MAYAN_BROKER_URL`
|
||||
### `MAYAN_CELERY_BROKER_URL`
|
||||
|
||||
Defaults to 'redis://127.0.0.1:6379/0'. This optional environment variable is determines the broker that Celery will use to relay task messages between the frontend code and the background workers. For more information read the pertinent Celery Kombu documentation page: [Broker URL](http://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
||||
|
||||
This Docker image supports using Redis and RabbitMQ as brokers.
|
||||
|
||||
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
Caveat: If the `MAYAN_CELERY_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
|
||||
### `MAYAN_CELERY_RESULT_BACKEND`
|
||||
|
||||
@@ -140,7 +140,7 @@ Defaults to 'redis://127.0.0.1:6379/0'. This optional environment variable is de
|
||||
|
||||
This Docker image supports using Redis and RabbitMQ as result backends.
|
||||
|
||||
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
Caveat: If the `MAYAN_CELERY_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
|
||||
### `MAYAN_NGINX_CLIENT_MAX_BODY_SIZE`
|
||||
|
||||
|
||||
@@ -126,13 +126,13 @@ Defaults to `None`. This optional environment variable is used to set the hostna
|
||||
|
||||
Defaults to `None`. This optional environment variable is used to set the port number to use when connecting to the database. An empty string means the default port. Not used with SQLite. For more information read the pertinent Django documentation page: [Settings, PORT](https://docs.djangoproject.com/en/1.11/ref/settings/#port)
|
||||
|
||||
### `MAYAN_BROKER_URL`
|
||||
### `MAYAN_CELERY_BROKER_URL`
|
||||
|
||||
Defaults to 'redis://127.0.0.1:6379/0'. This optional environment variable is determines the broker that Celery will use to relay task messages between the frontend code and the background workers. For more information read the pertinent Celery Kombu documentation page: [Broker URL](http://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
||||
|
||||
This Docker image supports using Redis and RabbitMQ as brokers.
|
||||
|
||||
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
Caveat: If the `MAYAN_CELERY_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
|
||||
### `MAYAN_CELERY_RESULT_BACKEND`
|
||||
|
||||
@@ -140,7 +140,7 @@ Defaults to 'redis://127.0.0.1:6379/0'. This optional environment variable is de
|
||||
|
||||
This Docker image supports using Redis and RabbitMQ as result backends.
|
||||
|
||||
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
Caveat: If the `MAYAN_CELERY_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment variables are specified, the built-in Redis server inside the container will be disabled.
|
||||
|
||||
### `MAYAN_NGINX_CLIENT_MAX_BODY_SIZE`
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
results:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||
MAYAN_CELERY_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
|
||||
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
|
||||
MAYAN_DATABASE_HOST: db
|
||||
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
results:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||
MAYAN_CELERY_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
|
||||
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
|
||||
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
|
||||
MAYAN_DATABASE_HOST: db
|
||||
|
||||
@@ -6,12 +6,12 @@ INSTALL_FLAG=/var/lib/mayan/system/SECRET_KEY
|
||||
CONCURRENCY_ARGUMENT=--concurrency=
|
||||
export DOCKER_ROOT=/opt/mayan-edms
|
||||
|
||||
export MAYAN_DEFAULT_BROKER_URL=redis://127.0.0.1:6379/0
|
||||
export MAYAN_DEFAULT_CELERY_BROKER_URL=redis://127.0.0.1:6379/0
|
||||
export MAYAN_DEFAULT_CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/0
|
||||
|
||||
export MAYAN_ALLOWED_HOSTS='["*"]'
|
||||
export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py
|
||||
export MAYAN_BROKER_URL=${MAYAN_BROKER_URL:-${MAYAN_DEFAULT_BROKER_URL}}
|
||||
export MAYAN_CELERY_BROKER_URL=${MAYAN_CELERY_BROKER_URL:-${MAYAN_DEFAULT_CELERY_BROKER_URL}}
|
||||
export MAYAN_CELERY_RESULT_BACKEND=${MAYAN_CELERY_RESULT_BACKEND:-${MAYAN_DEFAULT_CELERY_RESULT_BACKEND}}
|
||||
export MAYAN_INSTALL_DIR=/opt/mayan-edms
|
||||
export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
|
||||
@@ -55,13 +55,13 @@ chown mayan:mayan /var/lib/mayan -R
|
||||
initialize() {
|
||||
echo "mayan: initialize()"
|
||||
su mayan -c "${MAYAN_BIN} initialsetup --force"
|
||||
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear"
|
||||
su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
|
||||
}
|
||||
|
||||
upgrade() {
|
||||
echo "mayan: upgrade()"
|
||||
su mayan -c "${MAYAN_BIN} performupgrade"
|
||||
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear"
|
||||
su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
|
||||
}
|
||||
|
||||
start() {
|
||||
|
||||
@@ -12,7 +12,7 @@ user = mayan
|
||||
[program:redis]
|
||||
autorestart = false
|
||||
autostart = true
|
||||
command = /bin/bash -c "if [ ${MAYAN_BROKER_URL} == ${MAYAN_DEFAULT_BROKER_URL} ] && [ ${MAYAN_CELERY_RESULT_BACKEND} == ${MAYAN_DEFAULT_CELERY_RESULT_BACKEND} ];then /usr/bin/redis-server /etc/redis/;fi"
|
||||
command = /bin/bash -c "if [ ${MAYAN_CELERY_BROKER_URL} == ${MAYAN_DEFAULT_BROKER_URL} ] && [ ${MAYAN_CELERY_RESULT_BACKEND} == ${MAYAN_DEFAULT_CELERY_RESULT_BACKEND} ];then /usr/bin/redis-server /etc/redis/;fi"
|
||||
stderr_logfile = /dev/fd/2
|
||||
stderr_logfile_maxbytes = 0
|
||||
stdout_logfile = /dev/fd/1
|
||||
@@ -22,7 +22,7 @@ user = root
|
||||
[program:mayan-worker-fast]
|
||||
autorestart = false
|
||||
autostart = true
|
||||
command = nice -n 1 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -Ofair -l ERROR -Q converter,sources_fast -n mayan-worker-fast.%%h ${MAYAN_WORKER_FAST_CONCURRENCY}"
|
||||
command = nice -n 1 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -l ERROR -Q converter,document_states_fast,sources_fast -n mayan-worker-fast.%%h ${MAYAN_WORKER_FAST_CONCURRENCY}"
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -37,7 +37,7 @@ user = mayan
|
||||
[program:mayan-worker-medium]
|
||||
autorestart = false
|
||||
autostart = true
|
||||
command = nice -n 18 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -Ofair -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h ${MAYAN_WORKER_MEDIUM_CONCURRENCY}"
|
||||
command = nice -n 18 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h ${MAYAN_WORKER_MEDIUM_CONCURRENCY}"
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -52,7 +52,7 @@ user = mayan
|
||||
[program:mayan-worker-slow]
|
||||
autorestart = false
|
||||
autostart = true
|
||||
command = nice -n 19 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -Ofair -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h ${MAYAN_WORKER_SLOW_CONCURRENCY}"
|
||||
command = nice -n 19 /bin/bash -c "${MAYAN_BIN} celery --settings=${MAYAN_SETTINGS_MODULE} worker -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h ${MAYAN_WORKER_SLOW_CONCURRENCY}"
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
|
||||
@@ -136,8 +136,8 @@ Views
|
||||
|
||||
The module common.generics provides custom generic class based views to be used.
|
||||
The basic views used to create, edit, view and delete objects in Mayan EDMS
|
||||
are: SingleObjectCreateView, SingleObjectDetailView, SingleObjectEditView,
|
||||
and SingleObjectListView
|
||||
are: ``SingleObjectCreateView``, ``SingleObjectDetailView``,
|
||||
``SingleObjectEditView``, and ``SingleObjectListView``.
|
||||
|
||||
These views handle aspects relating to view permissions, object permissions,
|
||||
post action redirection and template context generation.
|
||||
|
||||
@@ -21,10 +21,10 @@ Binary dependencies
|
||||
If using a Debian_ or Ubuntu_ based Linux distribution, get the executable
|
||||
requirements using::
|
||||
|
||||
sudo apt-get install g++ gcc ghostscript gnupg1 graphviz libfuse2 \
|
||||
libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice libtiff-dev \
|
||||
poppler-utils postgresql python-dev python-virtualenv redis-server \
|
||||
sane-utils supervisor tesseract-ocr zlib1g-dev -y
|
||||
sudo apt-get install exiftool g++ gcc ghostscript gnupg1 graphviz \
|
||||
libfuse2 libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice \
|
||||
libtiff-dev poppler-utils postgresql python-dev python-virtualenv \
|
||||
redis-server sane-utils supervisor tesseract-ocr zlib1g-dev -y
|
||||
|
||||
Create an user account for the installation:
|
||||
--------------------------------------------
|
||||
@@ -73,9 +73,8 @@ Initialize the project:
|
||||
-----------------------
|
||||
::
|
||||
|
||||
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
sudo -u mayan MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, HOST=127.0.0.1}}' \
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py initialsetup
|
||||
|
||||
Collect the static files:
|
||||
@@ -83,7 +82,7 @@ Collect the static files:
|
||||
::
|
||||
|
||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py collectstatic --noinput
|
||||
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
|
||||
|
||||
Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
--------------------------------------------------------------------
|
||||
@@ -92,16 +91,11 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[supervisord]
|
||||
environment=
|
||||
MAYAN_ALLOWED_HOSTS='["*"]', # Allow access to other network hosts other than localhost
|
||||
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||
MAYAN_CELERY_RESULT_BACKEND="redis://127.0.0.1:6379/0",
|
||||
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0",
|
||||
PYTHONPATH=/opt/mayan-edms/lib/python2.7/site-packages:/opt/mayan-edms/data,
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media,
|
||||
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql,
|
||||
MAYAN_DATABASE_HOST=127.0.0.1,
|
||||
MAYAN_DATABASE_NAME=mayan,
|
||||
MAYAN_DATABASE_PASSWORD=mayanuserpass,
|
||||
MAYAN_DATABASE_USER=mayan,
|
||||
MAYAN_DATABASE_CONN_MAX_AGE=60,
|
||||
MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, HOST: 127.0.0.1, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, CONN_MAX_AGE: 60}}',
|
||||
DJANGO_SETTINGS_MODULE=mayan.settings.production
|
||||
|
||||
[program:mayan-gunicorn]
|
||||
@@ -113,7 +107,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[program:mayan-worker-fast]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q converter,sources_fast -n mayan-worker-fast.%%h --concurrency=1
|
||||
command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -l ERROR -Q converter,document_states_fast,sources_fast -n mayan-worker-fast.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -124,7 +118,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[program:mayan-worker-medium]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 18 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h --concurrency=1
|
||||
command = nice -n 18 /opt/mayan-edms/bin/mayan-edms.py celery worker -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -135,7 +129,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[program:mayan-worker-slow]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 19 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h --concurrency=1
|
||||
command = nice -n 19 /opt/mayan-edms/bin/mayan-edms.py celery worker -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -188,10 +182,11 @@ Binary dependencies
|
||||
If using a Debian_ or Ubuntu_ based Linux distribution, get the executable
|
||||
requirements using::
|
||||
|
||||
sudo apt-get install g++ gcc ghostscript gnupg1 graphviz libfuse2 \
|
||||
libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice libtiff-dev \
|
||||
poppler-utils postgresql python-dev python-virtualenv rabbitmq-server \
|
||||
redis-server sane-utils supervisor tesseract-ocr zlib1g-dev -y
|
||||
sudo apt-get install exiftool g++ gcc ghostscript gnupg1 graphviz \
|
||||
libfuse2 libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice \
|
||||
libtiff-dev poppler-utils postgresql python-dev python-virtualenv \
|
||||
rabbitmq-server redis-server sane-utils supervisor tesseract-ocr \
|
||||
zlib1g-dev -y
|
||||
|
||||
Create an user account for the installation:
|
||||
--------------------------------------------
|
||||
@@ -240,9 +235,8 @@ Initialize the project:
|
||||
-----------------------
|
||||
::
|
||||
|
||||
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \
|
||||
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
|
||||
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
sudo -u mayan MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, HOST=127.0.0.1}}' \
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py initialsetup
|
||||
|
||||
Collect the static files:
|
||||
@@ -250,7 +244,7 @@ Collect the static files:
|
||||
::
|
||||
|
||||
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
|
||||
/opt/mayan-edms/bin/mayan-edms.py collectstatic --noinput
|
||||
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
|
||||
|
||||
Create the RabbitMQ user and vhost:
|
||||
-----------------------------------
|
||||
@@ -267,16 +261,11 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[supervisord]
|
||||
environment=
|
||||
MAYAN_ALLOWED_HOSTS='["*"]', # Allow access to other network hosts other than localhost
|
||||
MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
MAYAN_CELERY_RESULT_BACKEND="redis://127.0.0.1:6379/0",
|
||||
MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
PYTHONPATH=/opt/mayan-edms/lib/python2.7/site-packages:/opt/mayan-edms/data,
|
||||
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media,
|
||||
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql,
|
||||
MAYAN_DATABASE_HOST=127.0.0.1,
|
||||
MAYAN_DATABASE_NAME=mayan,
|
||||
MAYAN_DATABASE_PASSWORD=mayanuserpass,
|
||||
MAYAN_DATABASE_USER=mayan,
|
||||
MAYAN_DATABASE_CONN_MAX_AGE=360,
|
||||
MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, HOST: 127.0.0.1, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, CONN_MAX_AGE: 60}}',
|
||||
DJANGO_SETTINGS_MODULE=mayan.settings.production
|
||||
|
||||
[program:mayan-gunicorn]
|
||||
@@ -288,7 +277,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[program:mayan-worker-fast]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q converter,sources_fast -n mayan-worker-fast.%%h
|
||||
command = nice -n 1 /opt/mayan-edms/bin/mayan-edms.py celery worker -l ERROR -Q converter,document_states_fast,sources_fast -n mayan-worker-fast.%%h
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -299,7 +288,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[program:mayan-worker-medium]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 18 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h --concurrency=1
|
||||
command = nice -n 18 /opt/mayan-edms/bin/mayan-edms.py celery worker -l ERROR -Q checkouts_periodic,documents_periodic,indexing,metadata,sources,sources_periodic,uploads,documents -n mayan-worker-medium.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -310,7 +299,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
|
||||
[program:mayan-worker-slow]
|
||||
autorestart = true
|
||||
autostart = true
|
||||
command = nice -n 19 /opt/mayan-edms/bin/mayan-edms.py celery worker -Ofair -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h --concurrency=1
|
||||
command = nice -n 19 /opt/mayan-edms/bin/mayan-edms.py celery worker -l ERROR -Q mailing,tools,statistics,parsing,ocr -n mayan-worker-slow.%%h --concurrency=1
|
||||
killasgroup = true
|
||||
numprocs = 1
|
||||
priority = 998
|
||||
@@ -345,6 +334,36 @@ Enable and restart the services [1_]:
|
||||
systemctl enable supervisor
|
||||
systemctl restart supervisor
|
||||
|
||||
|
||||
Troubleshooting
|
||||
===============
|
||||
|
||||
- Due to OS differences some binaries might reside in different locations.
|
||||
Use environment variables or the configuration file to tell Mayan EDMS where
|
||||
to file these binaries.
|
||||
|
||||
Example: OpenBSD. Add the following entries to supervisor configuration files.
|
||||
::
|
||||
|
||||
MAYAN_DOCUMENT_PARSING_PDFTOTEXT_PATH=/usr/local/bin/pdftotext,
|
||||
MAYAN_SIGNATURES_GPG_PATH=/usr/local/bin/gpg,
|
||||
MAYAN_SOURCES_SCANIMAGE_PATH: /usr/local/bin/scanimage,
|
||||
|
||||
Alternatively a symlink from the actual binary location to where Mayan
|
||||
EDMS is expecting them to be found by default also works for some users::
|
||||
|
||||
ln -s /usr/local/bin/gpg /usr/bin/gpg1
|
||||
|
||||
Example 2: Ubuntu 16.04. Add the following entries to supervisor
|
||||
configuration files.
|
||||
::
|
||||
|
||||
MAYAN_SIGNATURES_GPG_PATH=/usr/bin/gpg1,
|
||||
|
||||
Or add a symlink::
|
||||
|
||||
ln -s /usr/bin/gpg /usr/bin/gpg1
|
||||
|
||||
[1]: https://bugs.launchpad.net/ubuntu/+source/supervisor/+bug/1594740
|
||||
|
||||
.. _Debian: https://www.debian.org/
|
||||
|
||||
@@ -447,13 +447,28 @@ Version numbering
|
||||
=================
|
||||
|
||||
Mayan EDMS uses the Semantic Versioning (http://semver.org/) method to choose
|
||||
version numbers along with Python's PEP-0440 (https://www.python.org/dev/peps/pep-0440/)
|
||||
to format them.
|
||||
version numbers along with Python's PEP-0440
|
||||
(https://www.python.org/dev/peps/pep-0440/) to format them.
|
||||
|
||||
X.YaN # Alpha release
|
||||
X.YbN # Beta release
|
||||
X.YrcN # Release Candidate
|
||||
X.Y # Final release
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
| Version number | Name | Description |
|
||||
+================+===================+=========================================+
|
||||
| X.YalphaN | Alpha release | Usable but unstable, API changes. |
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
| X.YbetaN | Beta release | Code is frozen, testing. |
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
| X.YrcN | Release Candidate | Almost ready for production, not major |
|
||||
| | | changes between this version and the |
|
||||
| | | final release. |
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
| X.Y | Final release | API changes, many backward incompatible |
|
||||
| | | changes. |
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
| X.Y+1 | Minor release | Minor changes, minor backwards |
|
||||
| | | incompatible changes |
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
| X.Y.Z | Micro release | Minor changes, bugfixes. |
|
||||
+----------------+-------------------+-----------------------------------------+
|
||||
|
||||
|
||||
Release checklist
|
||||
|
||||
@@ -1,118 +1,10 @@
|
||||
============
|
||||
************
|
||||
Docker image
|
||||
============
|
||||
************
|
||||
|
||||
How to use this image
|
||||
=====================
|
||||
|
||||
.. _docker_install:
|
||||
|
||||
Start a Mayan EDMS image
|
||||
------------------------
|
||||
|
||||
With Docker properly installed, proceed to download the Mayan EDMS image using the command::
|
||||
|
||||
docker pull mayanedms/mayanedms:<version>
|
||||
|
||||
Then download version 9.5 of the Docker PostgreSQL image::
|
||||
|
||||
docker pull postgres:9.5
|
||||
|
||||
Create and run a PostgreSQL container::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms-postgres \
|
||||
--restart=always \
|
||||
-p 5432:5432 \
|
||||
-e POSTGRES_USER=mayan \
|
||||
-e POSTGRES_DB=mayan \
|
||||
-e POSTGRES_PASSWORD=mayanuserpass \
|
||||
-v /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data \
|
||||
-d postgres:9.5
|
||||
|
||||
The PostgreSQL container will have one database named ``mayan``, with an user
|
||||
named ``mayan`` too, with a password of ``mayanuserpass``. The container will
|
||||
expose its internal 5432 port (PostgreSQL's default port) via the host's
|
||||
5432 port. The data of this container will reside on the host's
|
||||
``/docker-volumes/mayan-edms/postgres`` folder.
|
||||
|
||||
Finally create and run a Mayan EDMS container. Change <version> with the
|
||||
latest version in numeric form (example: 2.7.3) or use the ``latest``
|
||||
identifier::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
|
||||
-e MAYAN_DATABASE_HOST=172.17.0.1 \
|
||||
-e MAYAN_DATABASE_NAME=mayan \
|
||||
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
|
||||
-e MAYAN_DATABASE_USER=mayan \
|
||||
-e MAYAN_DATABASE_CONN_MAX_AGE=60 \
|
||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||
mayanedms/mayanedms:<version>
|
||||
|
||||
The Mayan EDMS container will connect to the PostgreSQL container via the
|
||||
``172.17.0.1`` IP address (the Docker host's default IP address). It will
|
||||
connect using the ``django.db.backends.postgresql`` database driver and
|
||||
connect to the ``mayan`` database using the ``mayan`` user with the password
|
||||
``mayanuserpass``. The container will keep connections to the database
|
||||
for up to 60 seconds in an attempt to reuse them increasing response time
|
||||
and reducing memory usage. The files of the container will be store in the
|
||||
host's ``/docker-volumes/mayan-edms/media`` folder. The container will
|
||||
expose its web service running on port 8000 on the host's port 80.
|
||||
|
||||
The container will be available by browsing to ``http://localhost`` or to
|
||||
the IP address of the computer running the container.
|
||||
|
||||
If another web server is running on port 80 use a different port in the
|
||||
``-p`` option. For example: ``-p 81:8000``.
|
||||
|
||||
|
||||
Using a dedicated Docker network
|
||||
--------------------------------
|
||||
Use this method to avoid having to expose PostreSQL port to the host's network
|
||||
or if you have other PostgreSQL instances but still want to use the default
|
||||
port of 5432 for this installation.
|
||||
|
||||
Create the network::
|
||||
|
||||
docker network create mayan
|
||||
|
||||
Launch the PostgreSQL container with the network option and remove the port
|
||||
binding (``-p 5432:5432``)::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms-postgres \
|
||||
--network=mayan \
|
||||
--restart=always \
|
||||
-e POSTGRES_USER=mayan \
|
||||
-e POSTGRES_DB=mayan \
|
||||
-e POSTGRES_PASSWORD=mayanuserpass \
|
||||
-v /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data \
|
||||
-d postgres:9.5
|
||||
|
||||
Launch the Mayan EDMS container with the network option and change the
|
||||
database hostname to the PostgreSQL container name (``mayan-edms-postgres``)
|
||||
instead of the IP address of the Docker host (``172.17.0.1``)::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms \
|
||||
--network=mayan \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
|
||||
-e MAYAN_DATABASE_HOST=mayan-edms-postgres \
|
||||
-e MAYAN_DATABASE_NAME=mayan \
|
||||
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
|
||||
-e MAYAN_DATABASE_USER=mayan \
|
||||
-e MAYAN_DATABASE_CONN_MAX_AGE=60 \
|
||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||
mayanedms/mayanedms:<version>
|
||||
|
||||
Stopping and starting the container
|
||||
-----------------------------------
|
||||
===================================
|
||||
|
||||
To stop the container use::
|
||||
|
||||
@@ -127,9 +19,11 @@ To start the container again::
|
||||
.. _docker_environment_variables:
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
=====================
|
||||
|
||||
The Mayan EDMS image can be configure via environment variables.
|
||||
In addition to the all the environment variables supported by Mayan EDMS, the
|
||||
Mayan EDMS image provides some additional variables to configure the Docker
|
||||
specifics of the image.
|
||||
|
||||
``MAYAN_DATABASE_ENGINE``
|
||||
|
||||
@@ -145,27 +39,6 @@ When using the SQLite backend, the database file will be saved in the Docker
|
||||
volume. The SQLite database as used by Mayan EDMS is meant only for development
|
||||
or testing, never use it in production.
|
||||
|
||||
``MAYAN_DATABASE_NAME``
|
||||
|
||||
Defaults to 'mayan'. This optional environment variable can be used to define
|
||||
the database name that Mayan EDMS will connect to. For more information read
|
||||
the pertinent Django documentation page:
|
||||
:django-docs:`Connecting to the database <ref/databases/#connecting-to-the-database>`
|
||||
|
||||
``MAYAN_DATABASE_USER``
|
||||
|
||||
Defaults to 'mayan'. This optional environment variable is used to set the
|
||||
username that will be used to connect to the database. For more information
|
||||
read the pertinent Django documentation page:
|
||||
:django-docs:`Settings, USER <ref/settings/#user>`
|
||||
|
||||
``MAYAN_DATABASE_PASSWORD``
|
||||
|
||||
Defaults to ''. This optional environment variable is used to set the
|
||||
password that will be used to connect to the database. For more information
|
||||
read the pertinent Django documentation page:
|
||||
:django-docs:`Settings, PASSWORD <ref/settings/#password>`
|
||||
|
||||
``MAYAN_DATABASE_HOST``
|
||||
|
||||
Defaults to `None`. This optional environment variable is used to set the
|
||||
@@ -182,7 +55,7 @@ the default port. Not used with SQLite. For more information read the
|
||||
pertinent Django documentation page:
|
||||
:django-docs:`Settings, PORT <ref/settings/#port>`
|
||||
|
||||
``MAYAN_BROKER_URL``
|
||||
``MAYAN_CELERY_BROKER_URL``
|
||||
|
||||
This optional environment variable determines the broker that Celery will use
|
||||
to relay task messages between the frontend code and the background workers.
|
||||
@@ -192,7 +65,7 @@ For more information read the pertinent Celery Kombu documentation page: `Broker
|
||||
|
||||
This Docker image supports using Redis and RabbitMQ as brokers.
|
||||
|
||||
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
|
||||
Caveat: If the `MAYAN_CELERY_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
|
||||
variables are specified, the built-in Redis server inside the container will
|
||||
be disabled.
|
||||
|
||||
@@ -207,7 +80,7 @@ code. For more information read the pertinent Celery Kombu documentation page:
|
||||
|
||||
This Docker image supports using Redis and RabbitMQ as result backends.
|
||||
|
||||
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
|
||||
Caveat: If the `MAYAN_CELERY_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
|
||||
variables are specified, the built-in Redis server inside the container will
|
||||
be disabled.
|
||||
|
||||
@@ -215,12 +88,6 @@ be disabled.
|
||||
|
||||
Optional. Allows loading an alternate settings file.
|
||||
|
||||
``MAYAN_DATABASE_CONN_MAX_AGE``
|
||||
|
||||
Amount in seconds to keep a database connection alive. Allow reuse of database
|
||||
connections. For more information read the pertinent Django documentation
|
||||
page: :django-docs:`Settings, CONN_MAX_AGE <ref/settings/#conn-max-age>`
|
||||
|
||||
``MAYAN_GUNICORN_WORKERS``
|
||||
|
||||
Optional. This environment variable controls the number of frontend workers
|
||||
@@ -252,6 +119,7 @@ category. Default is 1. Use 0 to disable hardcoded concurrency and allow the
|
||||
Celery worker to launch its default number of child processes (equal to the
|
||||
number of CPUs detected).
|
||||
|
||||
|
||||
Accessing outside data
|
||||
======================
|
||||
|
||||
@@ -297,6 +165,7 @@ too need to be backed up using their respective procedures. A simple solution
|
||||
is to copy the entire database container volume after the container has
|
||||
been stopped.
|
||||
|
||||
|
||||
Restoring from a backup
|
||||
=======================
|
||||
|
||||
@@ -304,6 +173,7 @@ Uncompress the backup archive in the original docker volume using::
|
||||
|
||||
sudo tar -xvzf backup.tar.gz -C /
|
||||
|
||||
|
||||
Upgrading
|
||||
=========
|
||||
|
||||
@@ -333,6 +203,7 @@ Start the container again with the new image version::
|
||||
|
||||
docker run -d --name mayan-edms --restart=always -p 80:8000 -v /docker-volumes/mayan:/var/lib/mayan mayanedms/mayanedms:latest
|
||||
|
||||
|
||||
Building the image
|
||||
==================
|
||||
|
||||
@@ -355,9 +226,11 @@ Or using an apt cacher to speed up the build::
|
||||
Replace the IP address `172.17.0.1` with the IP address of the computer
|
||||
running the APT proxy and caching service.
|
||||
|
||||
|
||||
Customizing the image
|
||||
=====================
|
||||
|
||||
|
||||
Simple method
|
||||
-------------
|
||||
|
||||
@@ -379,6 +252,7 @@ Specifies a list of Python packages to be installed via ``pip``. Packages will
|
||||
be downloaded from the Python Package Index (https://pypi.python.org) by
|
||||
default.
|
||||
|
||||
|
||||
Using Docker compose
|
||||
====================
|
||||
|
||||
|
||||
109
docs/chapters/docker_installation.rst
Normal file
@@ -0,0 +1,109 @@
|
||||
*******************
|
||||
Docker installation
|
||||
*******************
|
||||
|
||||
Docker is a system that allows running programs in isolated areas which
|
||||
have restricted access to resources, devices, and memory. Docker usage also
|
||||
distributing software as a single file.
|
||||
|
||||
Make sure Docker is properly installed and working before attempting to install
|
||||
Mayan EDMS.
|
||||
|
||||
Docker can be installed using their automated script::
|
||||
|
||||
wget -qO- https://get.docker.com/ | sh
|
||||
|
||||
This installs the latest versions of Docker. If you don't want run an automated
|
||||
script follow the instructions outlined in their documentation:
|
||||
https://docs.docker.com/install/
|
||||
|
||||
With Docker properly installed, proceed to download the Mayan EDMS image using the command::
|
||||
|
||||
docker pull mayanedms/mayanedms:<version>
|
||||
|
||||
Then download version 9.5 of the Docker PostgreSQL image::
|
||||
|
||||
docker pull postgres:9.5
|
||||
|
||||
Create and run a PostgreSQL container::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms-postgres \
|
||||
--restart=always \
|
||||
-p 5432:5432 \
|
||||
-e POSTGRES_USER=mayan \
|
||||
-e POSTGRES_DB=mayan \
|
||||
-e POSTGRES_PASSWORD=mayanuserpass \
|
||||
-v /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data \
|
||||
-d postgres:9.5
|
||||
|
||||
The PostgreSQL container will have one database named ``mayan``, with an user
|
||||
named ``mayan`` too, with a password of ``mayanuserpass``. The container will
|
||||
expose its internal 5432 port (PostgreSQL's default port) via the host's
|
||||
5432 port. The data of this container will reside on the host's
|
||||
``/docker-volumes/mayan-edms/postgres`` folder.
|
||||
|
||||
Finally create and run a Mayan EDMS container. Change <version> with the
|
||||
latest version in numeric form (example: 2.7.3) or use the ``latest``
|
||||
identifier::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-e MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, HOST: 172.17.0.1, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, CONN_MAX_AGE: 60}}' \
|
||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||
mayanedms/mayanedms:<version>
|
||||
|
||||
The Mayan EDMS container will connect to the PostgreSQL container via the
|
||||
``172.17.0.1`` IP address (the Docker host's default IP address). It will
|
||||
connect using the ``django.db.backends.postgresql`` database driver and
|
||||
connect to the ``mayan`` database using the ``mayan`` user with the password
|
||||
``mayanuserpass``. The container will keep connections to the database
|
||||
for up to 60 seconds in an attempt to reuse them increasing response time
|
||||
and reducing memory usage. The files of the container will be store in the
|
||||
host's ``/docker-volumes/mayan-edms/media`` folder. The container will
|
||||
expose its web service running on port 8000 on the host's port 80.
|
||||
|
||||
The container will be available by browsing to ``http://localhost`` or to
|
||||
the IP address of the computer running the container.
|
||||
|
||||
If another web server is running on port 80 use a different port in the
|
||||
``-p`` option. For example: ``-p 81:8000``.
|
||||
|
||||
|
||||
Using a dedicated Docker network
|
||||
================================
|
||||
Use this method to avoid having to expose PostreSQL port to the host's network
|
||||
or if you have other PostgreSQL instances but still want to use the default
|
||||
port of 5432 for this installation.
|
||||
|
||||
Create the network::
|
||||
|
||||
docker network create mayan
|
||||
|
||||
Launch the PostgreSQL container with the network option and remove the port
|
||||
binding (``-p 5432:5432``)::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms-postgres \
|
||||
--network=mayan \
|
||||
--restart=always \
|
||||
-e POSTGRES_USER=mayan \
|
||||
-e POSTGRES_DB=mayan \
|
||||
-e POSTGRES_PASSWORD=mayanuserpass \
|
||||
-v /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data \
|
||||
-d postgres:9.5
|
||||
|
||||
Launch the Mayan EDMS container with the network option and change the
|
||||
database hostname to the PostgreSQL container name (``mayan-edms-postgres``)
|
||||
instead of the IP address of the Docker host (``172.17.0.1``)::
|
||||
|
||||
docker run -d \
|
||||
--name mayan-edms \
|
||||
--network=mayan \
|
||||
--restart=always \
|
||||
-p 80:8000 \
|
||||
-e MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, HOST: mayan-edms-postgres, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, CONN_MAX_AGE: 60}}' \
|
||||
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
|
||||
mayanedms/mayanedms:<version>
|
||||
@@ -86,11 +86,11 @@ Index by OCR content
|
||||
This example indexes documents in a "quarterly report" level if they have the
|
||||
fragment “quarterly report” in the OCR text::
|
||||
|
||||
{% if "quarterly report" in document.latest_version.ocr_content|join:" "|lower %}Quarterly reports{% endif %}
|
||||
{% if "quarterly report" in document.ocr_content.lower() %}Quarterly reports{% endif %}
|
||||
|
||||
The same applies to text content extracted for the document::
|
||||
|
||||
{% if "quarterly report" in document.latest_version.content|join:" "|lower %}Quarterly reports{% endif %}
|
||||
{% if "quarterly report" in document.content.lower() %}Quarterly reports{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ via the :ref:`configuration file <configuration_file>`.
|
||||
|
||||
Example::
|
||||
|
||||
DEFAULT_FROM_EMAIL: '<your administrator email>'
|
||||
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST: '<your smtp ip address or hostname>'
|
||||
EMAIL_HOST_PASSWORD: '<your smtp password>'
|
||||
|
||||
48
docs/chapters/password_validation.rst
Normal file
@@ -0,0 +1,48 @@
|
||||
*******************
|
||||
Password validation
|
||||
*******************
|
||||
|
||||
To help reduce the use of weak passwords, Mayan EDMS includes support for
|
||||
password validators. Password validator enforce policies by rejecting
|
||||
password that don't conform with the validator's logic.
|
||||
|
||||
By default, Mayan EDMS sets this password validation setup:
|
||||
|
||||
- That the password is not similar no any user attributes.
|
||||
- A minimum password size of 8 characters.
|
||||
- The password is not one of the 20,000 commonly used weak password.
|
||||
- That the password is not entirely numeric.
|
||||
|
||||
This default is coded in the following manner by the default Python setup file::
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
If using the YAML configuration file the same setup would be coded in the
|
||||
following manner::
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS:
|
||||
- NAME: django.contrib.auth.password_validation.UserAttributeSimilarityValidator
|
||||
- NAME: django.contrib.auth.password_validation.MinimumLengthValidator
|
||||
- NAME: django.contrib.auth.password_validation.CommonPasswordValidator
|
||||
- NAME: django.contrib.auth.password_validation.NumericPasswordValidator
|
||||
|
||||
In addition to the password validators provided by Django
|
||||
:django-docs:`validators provided by Django <topics/auth/passwords/#included-validators>`,
|
||||
Mayan EDMS adds the following validators:
|
||||
|
||||
.. autoclass:: mayan.apps.authentication.validators.MinimumCapitalLettersContentValidator
|
||||
|
||||
.. autoclass:: mayan.apps.authentication.validators.MinimumNumberContentValidator
|
||||
@@ -116,11 +116,11 @@ For the Docker image, launch a separate RabbitMQ container
|
||||
|
||||
docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3
|
||||
|
||||
Pass the MAYAN_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
||||
Pass the MAYAN_CELERY_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
|
||||
to the Mayan EDMS container so that it uses the RabbitMQ container the
|
||||
message broker::
|
||||
|
||||
-e MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
-e MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
|
||||
|
||||
When tasks finish, they leave behind a return status or the result of a
|
||||
calculation, these are stored for a while so that whoever requested the
|
||||
|
||||
@@ -37,6 +37,7 @@ http://yaml.org/). Here is an example of what the looks like::
|
||||
|
||||
DOCUMENT_PARSING_AUTO_PARSING: true
|
||||
DOCUMENT_PARSING_PDFTOTEXT_PATH: /usr/bin/pdftotext
|
||||
DEFAUL_FROM_EMAIL: mayan.admin@example.com
|
||||
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST: localhost
|
||||
EMAIL_HOST_PASSWORD: ''
|
||||
|
||||
@@ -15,7 +15,7 @@ The current document sources supported are:
|
||||
- IMAP email - Same as the ``POP3`` email source but for email accounts using
|
||||
the ``IMAP`` protocol.
|
||||
- Watch folder - A filesystem folder that is scanned periodically for files.
|
||||
Any file in the watch folder is automatically uploaded.
|
||||
Any file found in the watch folder is uploaded and subsequently deleted.
|
||||
- Staging folder - Folder where networked attached scanned can save image
|
||||
files. The files in these staging folders are scanned and a preview is
|
||||
generated to help the process of upload. Staging folders and Watch folders
|
||||
|
||||
89
docs/conf.py
@@ -13,7 +13,8 @@ from __future__ import unicode_literals
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
@@ -22,24 +23,25 @@ import mayan
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext")))
|
||||
sys.path.append(
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), '_ext'))
|
||||
)
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
#extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
|
||||
#extensions = ["djangodocs", "sphinx.ext.intersphinx"]
|
||||
extensions = [
|
||||
'sphinx.ext.extlinks', 'sphinxcontrib.blockdiag', 'sphinxcontrib.spelling'
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinxcontrib.blockdiag',
|
||||
'sphinxcontrib.spelling', 'sphinx.ext.viewcode'
|
||||
]
|
||||
|
||||
blockdiag_antialias = True
|
||||
blockdiag_html_image_format = "SVG"
|
||||
blockdiag_latex_image_format = "PDF"
|
||||
blockdiag_html_image_format = 'SVG'
|
||||
blockdiag_latex_image_format = 'PDF'
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -69,20 +71,20 @@ release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
@@ -99,7 +101,7 @@ show_authors = False
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
# modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
@@ -111,26 +113,26 @@ html_theme = 'classic'
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
@@ -139,44 +141,44 @@ html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'MayanEDMSdoc'
|
||||
@@ -188,41 +190,42 @@ html_show_sphinx = False
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
# latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#latex_font_size = '10pt'
|
||||
# latex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'MayanEDMS.tex', 'Mayan EDMS Documentation',
|
||||
mayan.__author__, 'manual'),
|
||||
(
|
||||
'index', 'MayanEDMS.tex', 'Mayan EDMS Documentation',
|
||||
mayan.__author__, 'manual'
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
# latex_show_urls = False
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#latex_preamble = ''
|
||||
# latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
# latex_domain_indices = True
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ for Mayan EDMS. Most MERCs will be Feature MERCs.
|
||||
2. An **Informational** MERC describes a Mayan EDMS design issue, or
|
||||
provides general guidelines or information to the Mayan EDMS community,
|
||||
but does not propose a new feature. Informational MERCs do not
|
||||
necessarily represent a community consensus or
|
||||
recommendation, so users and implementers are free to ignore
|
||||
Informational MERCs or follow their advice.
|
||||
necessarily represent a community consensus or recommendation, so users
|
||||
and implementers are free to ignore Informational MERCs or follow their
|
||||
advice.
|
||||
|
||||
3. A **Process** MERC describes a process surrounding Mayan EDMS, or
|
||||
proposes a change to (or an event in) a process. Process MERCs are
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
=====================
|
||||
====================
|
||||
MERC 2: Test writing
|
||||
=====================
|
||||
====================
|
||||
|
||||
:MERC: 2
|
||||
:Author: Michael Price
|
||||
|
||||
149
docs/mercs/0005-explicit-arguments.rst
Normal file
@@ -0,0 +1,149 @@
|
||||
==========================
|
||||
MERC 5: Explicit arguments
|
||||
==========================
|
||||
|
||||
:MERC: 5
|
||||
:Author: Roberto Rosario
|
||||
:Status: Accepted
|
||||
:Type: Feature
|
||||
:Created: 2018-12-30
|
||||
:Last-Modified: 2018-12-31
|
||||
|
||||
.. contents:: Table of Contents
|
||||
:depth: 3
|
||||
:local:
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This MERC proposes the adoption of a new methodology when performing calls.
|
||||
It seeks to reduce the use of positional arguments in favor of keyword
|
||||
arguments in as many places as possible.
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
As the project grows, legibility of code becomes more important. Keyword
|
||||
argument help document the use of services, clases and functions. Refactors
|
||||
that affect the interface of services are also easier to find and update and
|
||||
fix. Positional argument can cause a call to continue working as long as the
|
||||
datatype of the argument remains the same. Usage of keyword arguments will
|
||||
automatically raise and error that will prevent such situations. Keyword
|
||||
argument further eliminate the relevance of position or the arguments, and
|
||||
the arguments can be sorted alphabetically for easier visual scanning or by
|
||||
semantic significance improving code readability.
|
||||
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
Adoption of this MERC will require an audit of existing calls and the use
|
||||
of the method proposed for new calls. Every call regardless of the type or
|
||||
origin of the source callable will name each argument used. By type it is
|
||||
meant: classes, functions, methods. Origin means: local from the project,
|
||||
from the framework, third party libraries or the standard library.
|
||||
|
||||
|
||||
Backwards Compatibility
|
||||
=======================
|
||||
|
||||
No backwards compatibility issues are expected. New errors arising from the use
|
||||
if keyword arguments could be interpreted as existing latent issues that
|
||||
have not been uncovered.
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
Example:
|
||||
|
||||
Before:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from mayan.apps.common.classes import Template
|
||||
|
||||
Template(
|
||||
'menu_main', 'appearance/menu_main.html'
|
||||
)
|
||||
|
||||
|
||||
After:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from mayan.apps.common.classes import Template
|
||||
|
||||
Template(
|
||||
name='menu_main', template_name='appearance/menu_main.html'
|
||||
)
|
||||
|
||||
|
||||
When calls use a mixture or positional and keyword arguments, the keywords
|
||||
arguments can only be found after the positional arguments. Complete use
|
||||
of keyword arguments allow the reposition of arguments for semantic
|
||||
purposes.
|
||||
|
||||
Example:
|
||||
|
||||
Before:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import AboutView, HomeView, RootView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', RootView.as_view(), name='root'),
|
||||
url(r'^home/$', HomeView.as_view(), name='home'),
|
||||
url(r'^about/$', AboutView.as_view(), name='about_view'),
|
||||
]
|
||||
|
||||
|
||||
After:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import AboutView, HomeView, RootView
|
||||
|
||||
urlpatterns = [
|
||||
url(regex=r'^$', name='root', view=RootView.as_view()),
|
||||
url(regex=r'^home/$', name='home', view=HomeView.as_view()),
|
||||
url(regex=r'^about/$', name='about_view', view=AboutView.as_view()),
|
||||
]
|
||||
|
||||
|
||||
Keyword arguments should also be used for callables that pass those to others
|
||||
down the line like Django's ``reverse`` function. Any change to the name of
|
||||
the ``pk`` URL parameter will raise an exception in this code alerting to
|
||||
any posible incompatible use.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
viewname='documents:document_preview', kwargs={'pk': self.pk}
|
||||
)
|
||||
|
||||
|
||||
This becomes even more important when multiple URL parameters are used. Since
|
||||
the API documentation is auto generated from the code itself, it would make
|
||||
sense to rename the first URL parameter from ``pk`` to ``document_pk``. Such
|
||||
change will cause all address to view resolutions to break forcing their
|
||||
update and allowing all consumers' interface usage to remain synchonized to the
|
||||
callable's interface.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
url(
|
||||
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<document_version_pk>[0-9]+)/pages/(?P<document_page_pk>[0-9]+)/image/$',
|
||||
name='documentpage-image', view=APIDocumentPageImageView.as_view()
|
||||
),
|
||||
81
docs/mercs/0006-lower-information-disclose.rst
Normal file
@@ -0,0 +1,81 @@
|
||||
==================================
|
||||
MERC 6: Lower information disclose
|
||||
==================================
|
||||
|
||||
:MERC: 6
|
||||
:Author: Michael Price
|
||||
:Status: Accepted
|
||||
:Type: Feature
|
||||
:Created: 2018-12-30
|
||||
:Last-Modified: 2018-12-31
|
||||
|
||||
.. contents:: Table of Contents
|
||||
:depth: 3
|
||||
:local:
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This MERC proposes the use of errors that don't disclose the existance of a
|
||||
resource in the event that the requester doesn't have the required credentials.
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
When an user tries to perform an action like opening a view to a document for
|
||||
which the required permission is missing, a permission required or access
|
||||
denied error is presented. This is semantically correct, but from the stand
|
||||
point of security it is still failing because it is letting the user know
|
||||
that such document exists in the first place. This MERC proposes changing the
|
||||
error message for existing resource to one that doesn't divulge any information
|
||||
to unauthorized parties, like "Not Found".
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
Out of the 4 basic CRUD operations, Read, Update and Delete should return an
|
||||
HTTP 404 error instead of an HTTP 403 error. Only the Create operation will
|
||||
continue returning the current HTTP 403 error, unless it is creating a
|
||||
new resource that is related to an existing resource.
|
||||
|
||||
Since most view use the internal custom CRUD classes making a change to the
|
||||
``ObjectPermissionCheckMixin`` class to raise an HTTP 404 on object access
|
||||
failure will fulfill the proposal of this MERC.
|
||||
|
||||
Adding the ``object_permission_raise_404`` class attribute and setting it
|
||||
to default to False will allow fulfullin the goal of this MERC while
|
||||
keeping the existing functionality intact.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ObjectPermissionCheckMixin(object):
|
||||
"""
|
||||
If object_permission_raise_404 is True an HTTP 404 error will be raised
|
||||
instead of the normal 403.
|
||||
"""
|
||||
object_permission = None
|
||||
object_permission_raise_404 = False
|
||||
|
||||
def get_permission_object(self):
|
||||
return self.get_object()
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if self.object_permission:
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=self.object_permission, user=request.user,
|
||||
obj=self.get_permission_object(),
|
||||
related=getattr(self, 'object_permission_related', None)
|
||||
)
|
||||
except PermissionDenied:
|
||||
if self.object_permission_raise_404:
|
||||
raise Http404
|
||||
else:
|
||||
raise
|
||||
|
||||
return super(
|
||||
ObjectPermissionCheckMixin, self
|
||||
).dispatch(request, *args, **kwargs)
|
||||
@@ -20,6 +20,8 @@ Accepted
|
||||
../mercs/0001-merc-process
|
||||
../mercs/0002-test-writing
|
||||
../mercs/0003-using-javascript-libraries
|
||||
../mercs/0005-explicit-arguments
|
||||
../mercs/0006-lower-information-disclose
|
||||
|
||||
Draft
|
||||
-----
|
||||
@@ -49,3 +51,5 @@ Feature
|
||||
|
||||
../mercs/0002-test-writing
|
||||
../mercs/0003-using-javascript-libraries
|
||||
../mercs/0005-explicit-arguments
|
||||
../mercs/0006-lower-information-disclose
|
||||
|
||||
@@ -63,5 +63,5 @@ Changes needed:
|
||||
the Role model's permissions many to many field.
|
||||
4. Update the ``AccessControlList`` models roles field to point to the group
|
||||
models.
|
||||
5. Update the role checks in the ``check_access`` and ``filter_by_access``
|
||||
5. Update the role checks in the ``check_access`` and ``restrict_queryset``
|
||||
``AccessControlList`` model manager methods.
|
||||
|
||||
@@ -9,10 +9,10 @@ Changes
|
||||
|
||||
* Improve index mirroring value clean up code to remove the spaces at the
|
||||
starts and at the end of directories. Closes again GitLab issue #520
|
||||
Thanks to TheOneValen @ for the report.
|
||||
Thanks to @TheOneValen for the report.
|
||||
* Improve index mirroring cache class to use the hash of the keys
|
||||
instead of the literal keys. Avoid warning about invalid key
|
||||
characters. Closes GitLab issue #518. Thanks to TheOneValen @ for the
|
||||
characters. Closes GitLab issue #518. Thanks to @TheOneValen for the
|
||||
report.
|
||||
* Only render the Template API view for authenticated users.
|
||||
Thanks rgarcia for the report.
|
||||
|
||||
@@ -12,10 +12,10 @@ Changes
|
||||
* Remove duplicate YAML loading of environment variables.
|
||||
* Don't load development apps if they are already loaded.
|
||||
* Make sure all key used as input for the cache key hash are
|
||||
bytes and not unicode. GitLab issue #520. Thanks to TheOneValen
|
||||
@TheOneValen for the report.
|
||||
bytes and not unicode. GitLab issue #520. Thanks to @TheOneValen for
|
||||
the report.
|
||||
* Ignore document stub from the index mirror. GitLab issue
|
||||
#520. Thanks to TheOneValen @TheOneValen for the report.
|
||||
#520. Thanks to @TheOneValen for the report.
|
||||
* Fix for the Docker image INSTALL_FLAG path. Thanks to
|
||||
Mark Maglana @relaxdiego for the report and to Hamish Farroq @farroq_HAM
|
||||
for the patch. GitLab issue #525.
|
||||
|
||||
@@ -8,7 +8,8 @@ Changes
|
||||
-------
|
||||
|
||||
* Convert the furl instance to text to allow serializing it into
|
||||
JSON to be passed as arguments to the background task.
|
||||
JSON and be passed as arguments to the background task. Fixes
|
||||
metadata assignment issues when uploading new documents.
|
||||
|
||||
Removals
|
||||
--------
|
||||
|
||||
106
docs/releases/3.2.rst
Normal file
@@ -0,0 +1,106 @@
|
||||
Version 4.0
|
||||
===========
|
||||
|
||||
Released: XX XX, 2019
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
Switch to full app paths
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Instead of inserting the path of the apps into the Python app,
|
||||
the apps are now referenced by their full import path.
|
||||
|
||||
This solves name clashes with external or native Python libraries.
|
||||
Example: Mayan statistics app vs. Python new statistics library.
|
||||
|
||||
Every app reference is now prepended with 'mayan.apps'.
|
||||
|
||||
Existing config.yml files need to be updated manually.
|
||||
|
||||
|
||||
Other changes
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
* Split source models into different modules.
|
||||
* Fix multiple tag selection wizard step.
|
||||
|
||||
Removals
|
||||
--------
|
||||
|
||||
* Django suit
|
||||
* django-environ
|
||||
|
||||
|
||||
Upgrading from a previous version
|
||||
---------------------------------
|
||||
|
||||
If installed via Python's PIP
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Remove deprecated requirements::
|
||||
|
||||
$ curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt | pip uninstall -r /dev/stdin
|
||||
|
||||
Type in the console::
|
||||
|
||||
$ pip install mayan-edms==3.2
|
||||
|
||||
the requirements will also be updated automatically.
|
||||
|
||||
|
||||
Using Git
|
||||
^^^^^^^^^
|
||||
|
||||
If you installed Mayan EDMS by cloning the Git repository issue the commands::
|
||||
|
||||
$ git reset --hard HEAD
|
||||
$ git pull
|
||||
|
||||
otherwise download the compressed archived and uncompress it overriding the
|
||||
existing installation.
|
||||
|
||||
Remove deprecated requirements::
|
||||
|
||||
$ pip uninstall -y -r removals.txt
|
||||
|
||||
Next upgrade/add the new requirements::
|
||||
|
||||
$ pip install --upgrade -r requirements.txt
|
||||
|
||||
|
||||
Common steps
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Perform these steps after updating the code from either step above.
|
||||
|
||||
Migrate existing database schema with::
|
||||
|
||||
$ mayan-edms.py performupgrade
|
||||
|
||||
Add new static media::
|
||||
|
||||
$ mayan-edms.py preparestatic --noinput
|
||||
|
||||
The upgrade procedure is now complete.
|
||||
|
||||
|
||||
Backward incompatible changes
|
||||
-----------------------------
|
||||
|
||||
* None
|
||||
|
||||
|
||||
Bugs fixed or issues closed
|
||||
---------------------------
|
||||
|
||||
* :gitlab-issue:`395` Add support to limit the size of the cache.
|
||||
* :gitlab-issue:`487` gnupg1 Issue with Ubuntu 16.04 - Could not show/view documents
|
||||
* :gitlab-issue:`498` Can't scan subdirectories
|
||||
* :gitlab-issue:`522` Office 365 SMTP
|
||||
* :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified
|
||||
* :gitlab-issue:`539` Setting for default email sender is missing
|
||||
|
||||
|
||||
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
||||
@@ -20,6 +20,7 @@ versions of the documentation contain the release notes for any later releases.
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
3.2
|
||||
3.1.9
|
||||
3.1.8
|
||||
3.1.7
|
||||
|
||||
@@ -9,3 +9,4 @@ Administration
|
||||
.. include:: ../chapters/backups.rst
|
||||
.. include:: ../chapters/scaling_up.rst
|
||||
.. include:: ../chapters/database_conversion.rst
|
||||
.. include:: ../chapters/docker.rst
|
||||
|
||||
@@ -8,3 +8,4 @@ Advanced topics
|
||||
.. include:: ../chapters/metadata.rst
|
||||
.. include:: ../chapters/transformations.rst
|
||||
.. include:: ../chapters/versioning.rst
|
||||
.. include:: ../chapters/password_validation.rst
|
||||
|
||||
@@ -185,7 +185,7 @@ Django's development server doesn't serve static files unless the DEBUG option
|
||||
is set to True, this mode of operation should only be used for development or
|
||||
testing. For production deployments the management command::
|
||||
|
||||
$ mayan-edms.py collectstatic
|
||||
$ mayan-edms.py preparestatic
|
||||
|
||||
should be used and the resulting static folder served from a webserver.
|
||||
For more information check the
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
Installation
|
||||
############
|
||||
|
||||
The easiest way to use Mayan EDMS is by using the official Docker_ image.
|
||||
Make sure Docker is properly installed and working before attempting to install
|
||||
Mayan EDMS.
|
||||
Mayan EDMS can be install in several way. The two recommended ways are: by
|
||||
using Docker_, and by doing a direct installation.
|
||||
|
||||
The Docker method provides the easiest installation process while the direct
|
||||
installation provides better performance and customization.
|
||||
|
||||
*****************************
|
||||
Minimum hardware requirements
|
||||
@@ -15,34 +17,9 @@ Minimum hardware requirements
|
||||
- Unix-like operating system like Linux and OpenBSD. For other operating systems
|
||||
user container technologies like Docker or virtual machines.
|
||||
|
||||
****************
|
||||
Docker procedure
|
||||
****************
|
||||
|
||||
Docker is a computer program that performs operating-system-level
|
||||
virtualization also known as containerization. It allows independent
|
||||
"containers" to run within a single Linux instance, avoiding the overhead
|
||||
of starting and maintaining virtual machines (VMs).
|
||||
|
||||
Docker can be installed using their automated script::
|
||||
|
||||
wget -qO- https://get.docker.com/ | sh
|
||||
|
||||
This installs the latest versions of Docker. If you don't want run an automated
|
||||
script follow the instructions outlined in their documentation: https://docs.docker.com/install/
|
||||
|
||||
Once the Docker installation is finished, proceed to the link below to install
|
||||
the Docker image for Mayan EDMS.
|
||||
|
||||
Docker image chapter: :ref:`docker_install`
|
||||
|
||||
*******************
|
||||
Direct installation
|
||||
*******************
|
||||
|
||||
For users with knowledge of Python, Django, Ubuntu, and databases.
|
||||
|
||||
Deployments chapter: :doc:`../chapters/deploying`
|
||||
.. include:: ../chapters/docker_installation.rst
|
||||
.. include:: ../chapters/deploying.rst
|
||||
|
||||
|
||||
.. _Docker: https://www.docker.com/
|
||||
|
||||
@@ -2,4 +2,4 @@ from __future__ import unicode_literals
|
||||
|
||||
from .classes import ModelPermission # NOQA
|
||||
|
||||
default_app_config = 'acls.apps.ACLsApp'
|
||||
default_app_config = 'mayan.apps.acls.apps.ACLsApp'
|
||||
|
||||
@@ -1,203 +1,121 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from .models import AccessControlList
|
||||
from .permissions import permission_acl_edit, permission_acl_view
|
||||
from .serializers import (
|
||||
AccessControlListPermissionSerializer, AccessControlListSerializer,
|
||||
WritableAccessControlListPermissionSerializer,
|
||||
WritableAccessControlListSerializer
|
||||
from mayan.apps.common.mixins import ContentTypeViewMixin
|
||||
from mayan.apps.permissions.serializers import (
|
||||
PermissionSerializer, RolePermissionAddRemoveSerializer
|
||||
)
|
||||
from mayan.apps.rest_api.mixins import ExternalObjectAPIViewSetMixin
|
||||
from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet
|
||||
|
||||
from .permissions import permission_acl_edit, permission_acl_view
|
||||
from .serializers import AccessControlListSerializer
|
||||
|
||||
|
||||
class APIObjectACLListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
get: Returns a list of all the object's access control lists
|
||||
post: Create a new access control list for the selected object.
|
||||
"""
|
||||
def get_content_object(self):
|
||||
content_type = get_object_or_404(
|
||||
ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||
)
|
||||
|
||||
if self.request.method == 'GET':
|
||||
permission_required = permission_acl_view
|
||||
else:
|
||||
permission_required = permission_acl_edit
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_required, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_content_object().acls.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Extra context provided to the serializer class.
|
||||
"""
|
||||
context = super(APIObjectACLListView, self).get_serializer_context()
|
||||
if self.kwargs:
|
||||
context.update(
|
||||
{
|
||||
'content_object': self.get_content_object(),
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if not self.request:
|
||||
return None
|
||||
|
||||
return super(APIObjectACLListView, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return AccessControlListSerializer
|
||||
else:
|
||||
return WritableAccessControlListSerializer
|
||||
|
||||
|
||||
class APIObjectACLView(generics.RetrieveDestroyAPIView):
|
||||
"""
|
||||
delete: Delete the selected access control list.
|
||||
get: Returns the details of the selected access control list.
|
||||
"""
|
||||
class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet):
|
||||
content_type_url_kw_args = {
|
||||
'app_label': 'app_label',
|
||||
'model': 'model_name'
|
||||
}
|
||||
external_object_pk_url_kwarg = 'object_id'
|
||||
lookup_url_kwarg = 'acl_id'
|
||||
serializer_class = AccessControlListSerializer
|
||||
|
||||
def get_content_object(self):
|
||||
if self.request.method == 'GET':
|
||||
permission_required = permission_acl_view
|
||||
else:
|
||||
permission_required = permission_acl_edit
|
||||
|
||||
content_type = get_object_or_404(
|
||||
ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.validated_data.update(
|
||||
{
|
||||
'object_id': self.external_object.pk,
|
||||
'content_type': self.get_content_type(),
|
||||
}
|
||||
)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_required, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_content_object().acls.all()
|
||||
|
||||
|
||||
class APIObjectACLPermissionListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
get: Returns the access control list permission list.
|
||||
post: Add a new permission to the selected access control list.
|
||||
"""
|
||||
def get_acl(self):
|
||||
return get_object_or_404(
|
||||
self.get_content_object().acls, pk=self.kwargs['pk']
|
||||
)
|
||||
|
||||
def get_content_object(self):
|
||||
content_type = get_object_or_404(
|
||||
ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_view, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_acl().permissions.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if not self.request:
|
||||
def get_external_object_permission(self):
|
||||
action = getattr(self, 'action', None)
|
||||
if action is None:
|
||||
return None
|
||||
|
||||
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return AccessControlListPermissionSerializer
|
||||
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
|
||||
return permission_acl_view
|
||||
else:
|
||||
return WritableAccessControlListPermissionSerializer
|
||||
return permission_acl_edit
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super(APIObjectACLPermissionListView, self).get_serializer_context()
|
||||
if self.kwargs:
|
||||
context.update(
|
||||
{
|
||||
'acl': self.get_acl(),
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
|
||||
"""
|
||||
delete: Remove the permission from the selected access control list.
|
||||
get: Returns the details of the selected access control list permission.
|
||||
"""
|
||||
lookup_url_kwarg = 'permission_pk'
|
||||
serializer_class = AccessControlListPermissionSerializer
|
||||
|
||||
def get_acl(self):
|
||||
return get_object_or_404(
|
||||
self.get_content_object().acls, pk=self.kwargs['pk']
|
||||
)
|
||||
|
||||
def get_content_object(self):
|
||||
content_type = get_object_or_404(
|
||||
ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_view, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
def get_external_object_queryset(self):
|
||||
# Here we get a queryset the object model for which the event
|
||||
# will be accessed.
|
||||
return self.get_content_type().get_all_objects_for_this_type()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_acl().permissions.all()
|
||||
return self.get_external_object().acls.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super(APIObjectACLPermissionView, self).get_serializer_context()
|
||||
if self.kwargs:
|
||||
context.update(
|
||||
{
|
||||
'acl': self.get_acl(),
|
||||
}
|
||||
)
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id', methods=('post',),
|
||||
serializer_class=RolePermissionAddRemoveSerializer,
|
||||
url_name='permission-add', url_path='permissions/add'
|
||||
)
|
||||
def permission_add(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.permissions_add(instance=instance)
|
||||
headers = self.get_success_headers(data=serializer.data)
|
||||
return Response(
|
||||
serializer.data, headers=headers, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return context
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id',
|
||||
serializer_class=PermissionSerializer, url_name='permission-list',
|
||||
url_path='permissions'
|
||||
)
|
||||
def permission_list(self, request, *args, **kwargs):
|
||||
queryset = self.get_object().permissions.all()
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
queryset, many=True, context={'request': request}
|
||||
)
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id',
|
||||
serializer_class=PermissionSerializer,
|
||||
url_name='permission-inherited-list', url_path='permissions/inherited'
|
||||
)
|
||||
def permission_inherited_list(self, request, *args, **kwargs):
|
||||
queryset = self.get_object().get_inherited_permissions()
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
queryset, many=True, context={'request': request}
|
||||
)
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id',
|
||||
methods=('post',), serializer_class=RolePermissionAddRemoveSerializer,
|
||||
url_name='permission-remove', url_path='permissions/remove'
|
||||
)
|
||||
def permission_remove(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.permissions_remove(instance=instance)
|
||||
headers = self.get_success_headers(data=serializer.data)
|
||||
return Response(
|
||||
serializer.data, headers=headers, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
@@ -2,35 +2,55 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common import MayanAppConfig, menu_object, menu_sidebar
|
||||
from navigation import SourceColumn
|
||||
from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
|
||||
from mayan.apps.events import ModelEventType
|
||||
from mayan.apps.events.links import (
|
||||
link_events_for_object, link_object_event_types_user_subcriptions_list
|
||||
)
|
||||
from mayan.apps.navigation import SourceColumn
|
||||
|
||||
from .classes import ModelPermission
|
||||
from .events import event_acl_created, event_acl_edited
|
||||
from .links import link_acl_create, link_acl_delete, link_acl_permissions
|
||||
|
||||
|
||||
class ACLsApp(MayanAppConfig):
|
||||
app_namespace = 'acls'
|
||||
app_url = 'acls'
|
||||
has_rest_api = True
|
||||
has_tests = True
|
||||
name = 'acls'
|
||||
name = 'mayan.apps.acls'
|
||||
verbose_name = _('ACLs')
|
||||
|
||||
def ready(self):
|
||||
super(ACLsApp, self).ready()
|
||||
from actstream import registry
|
||||
|
||||
AccessControlList = self.get_model('AccessControlList')
|
||||
AccessControlList = self.get_model(model_name='AccessControlList')
|
||||
|
||||
SourceColumn(
|
||||
source=AccessControlList, label=_('Role'), attribute='role'
|
||||
ModelEventType.register(
|
||||
event_types=(event_acl_created, event_acl_edited),
|
||||
model=AccessControlList
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=AccessControlList, related='content_object',
|
||||
)
|
||||
|
||||
SourceColumn(
|
||||
source=AccessControlList, label=_('Permissions'),
|
||||
attribute='get_permission_titles'
|
||||
attribute='role', is_identifier=True, is_sortable=True,
|
||||
source=AccessControlList
|
||||
)
|
||||
|
||||
menu_object.bind_links(
|
||||
links=(link_acl_permissions, link_acl_delete),
|
||||
links=(
|
||||
link_acl_permissions, link_acl_delete,
|
||||
link_events_for_object,
|
||||
link_object_event_types_user_subcriptions_list
|
||||
),
|
||||
sources=(AccessControlList,)
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
menu_secondary.bind_links(
|
||||
links=(link_acl_create,), sources=('acls:acl_list',)
|
||||
)
|
||||
|
||||
registry.register(AccessControlList)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
@@ -8,23 +8,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelPermission(object):
|
||||
_registry = {}
|
||||
_proxies = {}
|
||||
_inheritances = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, model, permissions):
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
cls._registry.setdefault(model, [])
|
||||
for permission in permissions:
|
||||
cls._registry[model].append(permission)
|
||||
|
||||
AccessControlList = apps.get_model(
|
||||
app_label='acls', model_name='AccessControlList'
|
||||
)
|
||||
|
||||
model.add_to_class('acls', GenericRelation(AccessControlList))
|
||||
_proxies = {}
|
||||
_registry = {}
|
||||
|
||||
@classmethod
|
||||
def get_classes(cls, as_content_type=False):
|
||||
@@ -54,31 +40,38 @@ class ModelPermission(object):
|
||||
app_label='permissions', model_name='StoredPermission'
|
||||
)
|
||||
|
||||
permissions = []
|
||||
|
||||
class_permissions = cls.get_for_class(klass=type(instance))
|
||||
|
||||
if class_permissions:
|
||||
permissions.extend(class_permissions)
|
||||
|
||||
proxy = cls._proxies.get(type(instance))
|
||||
|
||||
if proxy:
|
||||
permissions.extend(cls._registry.get(proxy))
|
||||
permissions = cls.get_for_class(klass=type(instance))
|
||||
|
||||
pks = [
|
||||
permission.stored_permission.pk for permission in set(permissions)
|
||||
permission.stored_permission.pk for permission in permissions
|
||||
]
|
||||
return StoredPermission.objects.filter(pk__in=pks)
|
||||
|
||||
@classmethod
|
||||
def register_proxy(cls, source, model):
|
||||
cls._proxies[model] = source
|
||||
def get_inheritances(cls, model):
|
||||
return cls._inheritances[model]
|
||||
|
||||
@classmethod
|
||||
def register(cls, model, permissions):
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
|
||||
cls._registry.setdefault(model, [])
|
||||
for permission in permissions:
|
||||
cls._registry[model].append(permission)
|
||||
|
||||
AccessControlList = apps.get_model(
|
||||
app_label='acls', model_name='AccessControlList'
|
||||
)
|
||||
|
||||
model.add_to_class(
|
||||
name='acls', value=GenericRelation(to=AccessControlList)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def register_inheritance(cls, model, related):
|
||||
cls._inheritances[model] = related
|
||||
cls._inheritances.setdefault(model, [])
|
||||
cls._inheritances[model].append(related)
|
||||
|
||||
@classmethod
|
||||
def get_inheritance(cls, model):
|
||||
return cls._inheritances[model]
|
||||
def register_proxy(cls, source, model):
|
||||
cls._proxies[model] = source
|
||||
|
||||
16
mayan/apps/acls/events.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.events import EventTypeNamespace
|
||||
|
||||
namespace = EventTypeNamespace(
|
||||
label=_('Access control lists'), name='acls'
|
||||
)
|
||||
|
||||
event_acl_created = namespace.add_event_type(
|
||||
label=_('ACL created'), name='acl_created'
|
||||
)
|
||||
event_acl_edited = namespace.add_event_type(
|
||||
label=_('ACL edited'), name='acl_edited'
|
||||
)
|
||||
13
mayan/apps/acls/forms.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
|
||||
from mayan.apps.common.forms import FilteredSelectionForm
|
||||
|
||||
from .models import AccessControlList
|
||||
|
||||
|
||||
class ACLCreateForm(FilteredSelectionForm, forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ('role',)
|
||||
model = AccessControlList
|
||||
@@ -1,6 +1,10 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from appearance.classes import Icon
|
||||
from mayan.apps.appearance.classes import Icon
|
||||
|
||||
icon_acl_delete = Icon(driver_name='fontawesome', symbol='times')
|
||||
icon_acl_list = Icon(driver_name='fontawesome', symbol='lock')
|
||||
icon_acl_new = Icon(driver_name='fontawesome', symbol='plus')
|
||||
icon_acl_new = Icon(
|
||||
driver_name='fontawesome-dual', primary_symbol='lock',
|
||||
secondary_symbol='plus'
|
||||
)
|
||||
|
||||
@@ -3,10 +3,11 @@ from __future__ import unicode_literals
|
||||
from django.apps import apps
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from navigation import Link
|
||||
from mayan.apps.navigation import Link
|
||||
from mayan.apps.permissions.icons import icon_permission
|
||||
|
||||
from .icons import icon_acl_list, icon_acl_new
|
||||
from .permissions import permission_acl_view, permission_acl_edit
|
||||
from .icons import icon_acl_delete, icon_acl_list, icon_acl_new
|
||||
from .permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
|
||||
def get_kwargs_factory(variable_name):
|
||||
@@ -20,7 +21,7 @@ def get_kwargs_factory(variable_name):
|
||||
)
|
||||
return {
|
||||
'app_label': '"{}"'.format(content_type.app_label),
|
||||
'model': '"{}"'.format(content_type.model),
|
||||
'model_name': '"{}"'.format(content_type.model),
|
||||
'object_id': '{}.pk'.format(variable_name)
|
||||
}
|
||||
|
||||
@@ -28,21 +29,21 @@ def get_kwargs_factory(variable_name):
|
||||
|
||||
|
||||
link_acl_delete = Link(
|
||||
args='resolved_object.pk', permissions=(permission_acl_edit,),
|
||||
permissions_related='content_object', tags='dangerous', text=_('Delete'),
|
||||
icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'},
|
||||
permission=permission_acl_edit, tags='dangerous', text=_('Delete'),
|
||||
view='acls:acl_delete',
|
||||
)
|
||||
link_acl_list = Link(
|
||||
icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list'
|
||||
icon_class=icon_acl_list, kwargs=get_kwargs_factory(
|
||||
variable_name='resolved_object'
|
||||
), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list'
|
||||
)
|
||||
link_acl_create = Link(
|
||||
icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_acl_edit,), text=_('New ACL'),
|
||||
view='acls:acl_create'
|
||||
permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create'
|
||||
)
|
||||
link_acl_permissions = Link(
|
||||
args='resolved_object.pk', permissions=(permission_acl_edit,),
|
||||
permissions_related='content_object', text=_('Permissions'),
|
||||
args='resolved_object.pk', icon_class=icon_permission,
|
||||
permission=permission_acl_edit, text=_('Permissions'),
|
||||
view='acls:acl_permissions',
|
||||
)
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
from django.db.models import CharField, Value as V, Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import Http404
|
||||
|
||||
from common.utils import return_attrib, return_related
|
||||
from permissions import Permission
|
||||
from permissions.models import StoredPermission
|
||||
from mayan.apps.common.utils import (
|
||||
get_related_field, resolve_attribute, return_related
|
||||
)
|
||||
from mayan.apps.common.warnings import InterfaceWarning
|
||||
from mayan.apps.permissions import Permission
|
||||
from mayan.apps.permissions.models import StoredPermission
|
||||
|
||||
from .exceptions import PermissionNotValidForClass
|
||||
from .classes import ModelPermission
|
||||
from .exceptions import PermissionNotValidForClass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,200 +30,189 @@ class AccessControlListManager(models.Manager):
|
||||
Implement a 3 tier permission system, involving a permissions, an actor
|
||||
and an object
|
||||
"""
|
||||
def check_access(self, permissions, user, obj, related=None):
|
||||
if user.is_superuser or user.is_staff:
|
||||
logger.debug(
|
||||
'Permissions "%s" on "%s" granted to user "%s" as superuser '
|
||||
'or staff', permissions, obj, user
|
||||
def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None):
|
||||
"""
|
||||
This method does the bulk of the work. It generates filters for the
|
||||
AccessControlList model to determine if there are ACL entries for the
|
||||
members of the queryset's model provided.
|
||||
"""
|
||||
# Determine which of the cases we need to address
|
||||
# 1: No related field
|
||||
# 2: Related field
|
||||
# 3: Related field that is Generic Foreign Key
|
||||
# 4: No related field, but has an inherited related field, solved by
|
||||
# recursion, branches to #2 or #3.
|
||||
# 5: Inherited field of a related field
|
||||
# -- Not addressed yet --
|
||||
# 6: Inherited field of a related field that is Generic Foreign Key
|
||||
result = []
|
||||
|
||||
if related_field_name:
|
||||
related_field = get_related_field(
|
||||
model=queryset.model, related_field_name=related_field_name
|
||||
)
|
||||
return True
|
||||
|
||||
try:
|
||||
return Permission.check_permissions(
|
||||
requester=user, permissions=permissions
|
||||
)
|
||||
except PermissionDenied:
|
||||
try:
|
||||
stored_permissions = [
|
||||
permission.stored_permission for permission in permissions
|
||||
]
|
||||
except TypeError:
|
||||
# Not a list of permissions, just one
|
||||
stored_permissions = (permissions.stored_permission,)
|
||||
|
||||
if related:
|
||||
obj = return_attrib(obj, related)
|
||||
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(
|
||||
model=obj._meta.model
|
||||
)
|
||||
except AttributeError:
|
||||
# AttributeError means non model objects: ie Statistics
|
||||
# These can't have ACLs so we raise PermissionDenied
|
||||
raise PermissionDenied(_('Insufficient access for: %s') % obj)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
return self.check_access(
|
||||
obj=getattr(obj, parent_accessor),
|
||||
permissions=permissions, user=user
|
||||
if isinstance(related_field, GenericForeignKey):
|
||||
# Case 3: Generic Foreign Key, multiple ContentTypes + object
|
||||
# id combinations
|
||||
content_type_object_id_queryset = queryset.annotate(
|
||||
ct_fk_combination=Concat(
|
||||
related_field.ct_field, V('-'), related_field.fk_field,
|
||||
output_field=CharField()
|
||||
)
|
||||
except AttributeError:
|
||||
# Has no such attribute, try it as a related field
|
||||
try:
|
||||
return self.check_access(
|
||||
obj=return_related(
|
||||
instance=obj, related_field=parent_accessor
|
||||
), permissions=permissions, user=user
|
||||
)
|
||||
except PermissionDenied:
|
||||
pass
|
||||
except PermissionDenied:
|
||||
pass
|
||||
).values('ct_fk_combination')
|
||||
|
||||
user_roles = []
|
||||
for group in user.groups.all():
|
||||
for role in group.roles.all():
|
||||
if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))):
|
||||
logger.debug(
|
||||
'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL',
|
||||
permissions, obj, user, role
|
||||
)
|
||||
return True
|
||||
acl_filter = self.annotate(
|
||||
ct_fk_combination=Concat(
|
||||
'content_type', V('-'), 'object_id', output_field=CharField()
|
||||
)
|
||||
).filter(
|
||||
permissions=stored_permission, role__groups__user=user,
|
||||
ct_fk_combination__in=content_type_object_id_queryset
|
||||
).values('object_id')
|
||||
|
||||
user_roles.append(role)
|
||||
field_lookup = 'object_id__in'
|
||||
|
||||
if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
|
||||
logger.debug(
|
||||
'Permissions "%s" on "%s" denied for user "%s"',
|
||||
permissions, obj, user
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
else:
|
||||
# Case 2: Related field of a single type, single ContentType,
|
||||
# multiple object id
|
||||
content_type = ContentType.objects.get_for_model(
|
||||
model=related_field.related_model
|
||||
)
|
||||
raise PermissionDenied(ugettext('Insufficient access for: %s') % obj)
|
||||
field_lookup = '{}_id__in'.format(related_field_name)
|
||||
acl_filter = self.filter(
|
||||
content_type=content_type, permissions=stored_permission,
|
||||
role__groups__user=user
|
||||
).values('object_id')
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
# Case 5: Related field, has an inherited related field itself
|
||||
# Bubble up permssion check
|
||||
# TODO: Add relationship support: OR or AND
|
||||
# TODO: OR for document pages, version, doc, and types
|
||||
# TODO: AND for new cabinet levels ACLs
|
||||
try:
|
||||
related_field_model_related_fields = ModelPermission.get_inheritances(
|
||||
model=related_field.related_model
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
relation_result = []
|
||||
for related_field_model_related_field_name in related_field_model_related_fields:
|
||||
related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name)
|
||||
related_field_inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
user=user, related_field_name=related_field_name
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
|
||||
permissions, obj, user, user_roles
|
||||
)
|
||||
relation_result.append(reduce(operator.and_, related_field_inherited_acl_queries))
|
||||
|
||||
def filter_by_access(self, permission, user, queryset):
|
||||
if user.is_superuser or user.is_staff:
|
||||
logger.debug(
|
||||
'Unfiltered queryset returned to user "%s" as superuser or staff',
|
||||
user
|
||||
)
|
||||
return queryset
|
||||
|
||||
try:
|
||||
Permission.check_permissions(
|
||||
requester=user, permissions=(permission,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
user_roles = []
|
||||
for group in user.groups.all():
|
||||
for role in group.roles.all():
|
||||
user_roles.append(role)
|
||||
result.append(reduce(operator.or_, relation_result))
|
||||
else:
|
||||
# Case 1: Original model, single ContentType, multiple object id
|
||||
content_type = ContentType.objects.get_for_model(model=queryset.model)
|
||||
field_lookup = 'id__in'
|
||||
acl_filter = self.filter(
|
||||
content_type=content_type, permissions=stored_permission,
|
||||
role__groups__user=user
|
||||
).values('object_id')
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
|
||||
# Case 4: Original model, has an inherited related field
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(
|
||||
related_fields = ModelPermission.get_inheritances(
|
||||
model=queryset.model
|
||||
)
|
||||
except KeyError:
|
||||
parent_acl_query = Q()
|
||||
pass
|
||||
else:
|
||||
instance = queryset.first()
|
||||
if instance:
|
||||
parent_object = return_related(
|
||||
instance=instance, related_field=parent_accessor
|
||||
relation_result = []
|
||||
|
||||
for related_field_name in related_fields:
|
||||
inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
related_field_name=related_field_name, user=user
|
||||
)
|
||||
relation_result.append(reduce(operator.and_, inherited_acl_queries))
|
||||
|
||||
try:
|
||||
# Try to see if parent_object is a function
|
||||
parent_object()
|
||||
except TypeError:
|
||||
# Is not a function, try it as a field
|
||||
parent_content_type = ContentType.objects.get_for_model(
|
||||
parent_object
|
||||
)
|
||||
parent_queryset = self.filter(
|
||||
content_type=parent_content_type, role__in=user_roles,
|
||||
permissions=permission.stored_permission
|
||||
)
|
||||
parent_acl_query = Q(
|
||||
**{
|
||||
'{}__pk__in'.format(
|
||||
parent_accessor
|
||||
): parent_queryset.values_list(
|
||||
'object_id', flat=True
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Is a function. Can't perform Q object filtering.
|
||||
# Perform iterative filtering.
|
||||
result = []
|
||||
for entry in queryset:
|
||||
try:
|
||||
self.check_access(permissions=permission, user=user, obj=entry)
|
||||
except PermissionDenied:
|
||||
pass
|
||||
else:
|
||||
result.append(entry.pk)
|
||||
result.append(reduce(operator.or_, relation_result))
|
||||
|
||||
return queryset.filter(pk__in=result)
|
||||
else:
|
||||
parent_acl_query = Q()
|
||||
return result
|
||||
|
||||
# Directly granted access
|
||||
content_type = ContentType.objects.get_for_model(queryset.model)
|
||||
acl_query = Q(pk__in=self.filter(
|
||||
content_type=content_type, role__in=user_roles,
|
||||
permissions=permission.stored_permission
|
||||
).values_list('object_id', flat=True))
|
||||
logger.debug(
|
||||
'Filtered queryset returned to user "%s" based on roles "%s"',
|
||||
user, user_roles
|
||||
)
|
||||
def check_access(self, obj, permission, user, raise_404=False):
|
||||
warnings.warn(
|
||||
'check_access() is deprecated, use restrict_queryset() to '
|
||||
'produce a queryset from which to .get() the corresponding '
|
||||
'object in the local code.', InterfaceWarning
|
||||
)
|
||||
queryset = self.restrict_queryset(
|
||||
permission=permission, queryset=obj._meta.default_manager.all(),
|
||||
user=user
|
||||
)
|
||||
|
||||
return queryset.filter(parent_acl_query | acl_query)
|
||||
if queryset.filter(pk=obj.pk).exists():
|
||||
return True
|
||||
else:
|
||||
if raise_404:
|
||||
raise Http404
|
||||
else:
|
||||
raise PermissionDenied
|
||||
|
||||
def get_inherited_permissions(self, obj, role):
|
||||
queryset = self._get_inherited_object_permissions(obj=obj, role=role)
|
||||
|
||||
queryset = queryset | role.permissions.all()
|
||||
|
||||
# Filter the permissions to the ones that apply to the model
|
||||
queryset = ModelPermission.get_for_instance(
|
||||
instance=obj
|
||||
).filter(
|
||||
pk__in=queryset
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def _get_inherited_object_permissions(self, obj, role):
|
||||
queryset = StoredPermission.objects.none()
|
||||
|
||||
if not obj:
|
||||
return queryset
|
||||
|
||||
def get_inherited_permissions(self, role, obj):
|
||||
try:
|
||||
instance = obj.first()
|
||||
except AttributeError:
|
||||
instance = obj
|
||||
else:
|
||||
if not instance:
|
||||
return StoredPermission.objects.none()
|
||||
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(type(instance))
|
||||
related_fields = ModelPermission.get_inheritances(
|
||||
model=type(obj)
|
||||
)
|
||||
except KeyError:
|
||||
return StoredPermission.objects.none()
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
parent_object = return_attrib(
|
||||
obj=instance, attrib=parent_accessor
|
||||
)
|
||||
except AttributeError:
|
||||
# Parent accessor is not an attribute, try it as a related
|
||||
# field.
|
||||
parent_object = return_related(
|
||||
instance=instance, related_field=parent_accessor
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(parent_object)
|
||||
try:
|
||||
return self.get(
|
||||
role=role, content_type=content_type,
|
||||
object_id=parent_object.pk
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
return StoredPermission.objects.none()
|
||||
for related_field_name in related_fields:
|
||||
try:
|
||||
parent_object = resolve_attribute(
|
||||
obj=obj, attribute=related_field_name
|
||||
)
|
||||
except AttributeError:
|
||||
# Parent accessor is not an attribute, try it as a related
|
||||
# field.
|
||||
parent_object = return_related(
|
||||
instance=obj, related_field=related_field_name
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(model=parent_object)
|
||||
try:
|
||||
queryset = queryset | self.get(
|
||||
content_type=content_type, object_id=parent_object.pk,
|
||||
role=role
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
def grant(self, permission, role, obj):
|
||||
queryset = queryset | self._get_inherited_object_permissions(
|
||||
obj=parent_object, role=role
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def grant(self, obj, permission, role):
|
||||
class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
|
||||
if permission not in class_permissions:
|
||||
raise PermissionNotValidForClass
|
||||
@@ -229,7 +225,44 @@ class AccessControlListManager(models.Manager):
|
||||
|
||||
acl.permissions.add(permission.stored_permission)
|
||||
|
||||
def revoke(self, permission, role, obj):
|
||||
return acl
|
||||
|
||||
def restrict_queryset_by_accesses(self, operator, permissions, queryset, user):
|
||||
result = []
|
||||
|
||||
for permission in permissions:
|
||||
result.append(
|
||||
self.restrict_queryset(
|
||||
permission=permission, queryset=queryset, user=user
|
||||
)
|
||||
)
|
||||
|
||||
return reduce(operator, result)
|
||||
|
||||
def restrict_queryset(self, permission, queryset, user):
|
||||
# Check directly granted permission via a role
|
||||
try:
|
||||
Permission.check_user_permission(permission=permission, user=user)
|
||||
except PermissionDenied:
|
||||
acl_filters = self._get_acl_filters(
|
||||
queryset=queryset,
|
||||
stored_permission=permission.stored_permission, user=user
|
||||
)
|
||||
|
||||
final_query = None
|
||||
for acl_filter in acl_filters:
|
||||
if final_query is None:
|
||||
final_query = acl_filter
|
||||
else:
|
||||
final_query = final_query | acl_filter
|
||||
|
||||
return queryset.filter(final_query)
|
||||
else:
|
||||
# User has direct permission assignment via a role, is superuser or
|
||||
# is staff. Return the entire queryset.
|
||||
return queryset
|
||||
|
||||
def revoke(self, obj, permission, role):
|
||||
content_type = ContentType.objects.get_for_model(model=obj)
|
||||
acl, created = self.get_or_create(
|
||||
content_type=content_type, object_id=obj.pk,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.11 on 2018-04-02 03:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
@@ -14,6 +12,9 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='accesscontrollist',
|
||||
options={'ordering': ('pk',), 'verbose_name': 'Access entry', 'verbose_name_plural': 'Access entries'},
|
||||
options={
|
||||
'ordering': ('pk',), 'verbose_name': 'Access entry',
|
||||
'verbose_name_plural': 'Access entries'
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import operator
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from permissions.models import Role, StoredPermission
|
||||
from mayan.apps.permissions.models import Role, StoredPermission
|
||||
|
||||
from .events import event_acl_created, event_acl_edited
|
||||
from .managers import AccessControlListManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -29,6 +32,11 @@ class AccessControlList(models.Model):
|
||||
* Role - Custom role that is being granted a permission. Roles are created
|
||||
in the Setup menu.
|
||||
"""
|
||||
# Multiple inheritance operator types
|
||||
OPERATOR_AND = operator.and_
|
||||
OPERATOR_OR = operator.or_
|
||||
operator_default = OPERATOR_AND
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='object_content_type',
|
||||
to=ContentType
|
||||
@@ -57,21 +65,58 @@ class AccessControlList(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return _(
|
||||
'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"'
|
||||
'Role "%(role)s" permission\'s for "%(object)s"'
|
||||
) % {
|
||||
'permissions': self.get_permission_titles(),
|
||||
'object': self.content_object,
|
||||
'role': self.role
|
||||
'role': self.role,
|
||||
}
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
viewname='acls:acl_permissions', kwargs={'acl_id': self.pk}
|
||||
)
|
||||
|
||||
def get_inherited_permissions(self):
|
||||
return AccessControlList.objects.get_inherited_permissions(
|
||||
role=self.role, obj=self.content_object
|
||||
)
|
||||
|
||||
def get_permission_titles(self):
|
||||
"""
|
||||
Returns the descriptibe labels for the permissions.
|
||||
"""
|
||||
result = ', '.join(
|
||||
[force_text(permission) for permission in self.permissions.all()]
|
||||
)
|
||||
|
||||
return result or _('None')
|
||||
get_permission_titles.short_description = _('Permissions')
|
||||
|
||||
def permissions_add(self, queryset, _user=None):
|
||||
with transaction.atomic():
|
||||
event_acl_edited.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
self.permissions.add(*queryset)
|
||||
|
||||
def permissions_remove(self, queryset, _user=None):
|
||||
with transaction.atomic():
|
||||
event_acl_edited.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
self.permissions.remove(*queryset)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
_user = kwargs.pop('_user', None)
|
||||
|
||||
with transaction.atomic():
|
||||
is_new = not self.pk
|
||||
super(AccessControlList, self).save(*args, **kwargs)
|
||||
if is_new:
|
||||
event_acl_created.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
else:
|
||||
event_acl_edited.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
|
||||
@@ -2,13 +2,13 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from permissions import PermissionNamespace
|
||||
from mayan.apps.permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace('acls', _('Access control lists'))
|
||||
namespace = PermissionNamespace(label=_('Access control lists'), name='acls')
|
||||
|
||||
permission_acl_edit = namespace.add_permission(
|
||||
name='acl_edit', label=_('Edit ACLs')
|
||||
label=_('Edit ACLs'), name='acl_edit'
|
||||
)
|
||||
permission_acl_view = namespace.add_permission(
|
||||
name='acl_view', label=_('View ACLs')
|
||||
label=_('View ACLs'), name='acl_view'
|
||||
)
|
||||
|
||||
@@ -1,204 +1,143 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from common.serializers import ContentTypeSerializer
|
||||
from permissions import Permission
|
||||
from permissions.models import Role, StoredPermission
|
||||
from permissions.serializers import PermissionSerializer, RoleSerializer
|
||||
from mayan.apps.common.serializers import ContentTypeSerializer
|
||||
from mayan.apps.permissions.models import Role
|
||||
from mayan.apps.permissions.permissions import permission_role_edit
|
||||
from mayan.apps.permissions.serializers import RoleSerializer
|
||||
from mayan.apps.rest_api.mixins import ExternalObjectSerializerMixin
|
||||
from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField
|
||||
|
||||
from .models import AccessControlList
|
||||
|
||||
|
||||
class AccessControlListSerializer(serializers.ModelSerializer):
|
||||
class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer):
|
||||
content_type = ContentTypeSerializer(read_only=True)
|
||||
permissions_url = serializers.SerializerMethodField(
|
||||
help_text=_(
|
||||
'API URL pointing to the list of permissions for this access '
|
||||
'control list.'
|
||||
)
|
||||
)
|
||||
role = RoleSerializer(read_only=True)
|
||||
url = serializers.SerializerMethodField()
|
||||
permission_add_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-add'
|
||||
)
|
||||
permission_list_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-list'
|
||||
)
|
||||
permission_list_inherited_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-inherited-list'
|
||||
)
|
||||
permission_remove_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-remove'
|
||||
)
|
||||
role_id = serializers.CharField(
|
||||
label=_('Role ID'),
|
||||
help_text=_(
|
||||
'Primary key of the role of the ACL that will be created or edited.'
|
||||
), required=False, write_only=True
|
||||
)
|
||||
url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-detail'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
external_object_model = Role
|
||||
external_object_pk_field = 'role_id'
|
||||
external_object_permission = permission_role_edit
|
||||
fields = (
|
||||
'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url'
|
||||
'content_type', 'id', 'object_id', 'permission_add_url',
|
||||
'permission_list_url', 'permission_list_inherited_url',
|
||||
'permission_remove_url', 'role', 'role_id',
|
||||
'url'
|
||||
)
|
||||
model = AccessControlList
|
||||
|
||||
def get_permissions_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-permission-list', args=(
|
||||
instance.content_type.app_label, instance.content_type.model,
|
||||
instance.object_id, instance.pk
|
||||
), request=self.context['request'], format=self.context['format']
|
||||
)
|
||||
|
||||
def get_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-detail', args=(
|
||||
instance.content_type.app_label, instance.content_type.model,
|
||||
instance.object_id, instance.pk
|
||||
), request=self.context['request'], format=self.context['format']
|
||||
)
|
||||
|
||||
|
||||
class AccessControlListPermissionSerializer(PermissionSerializer):
|
||||
acl_permission_url = serializers.SerializerMethodField(
|
||||
help_text=_(
|
||||
'API URL pointing to a permission in relation to the '
|
||||
'access control list to which it is attached. This URL is '
|
||||
'different than the canonical workflow URL.'
|
||||
)
|
||||
)
|
||||
acl_url = serializers.SerializerMethodField()
|
||||
|
||||
def get_acl_permission_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-permission-detail', args=(
|
||||
self.context['acl'].content_type.app_label,
|
||||
self.context['acl'].content_type.model,
|
||||
self.context['acl'].object_id, self.context['acl'].pk,
|
||||
instance.stored_permission.pk
|
||||
), request=self.context['request'], format=self.context['format']
|
||||
)
|
||||
|
||||
def get_acl_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-detail', args=(
|
||||
self.context['acl'].content_type.app_label,
|
||||
self.context['acl'].content_type.model,
|
||||
self.context['acl'].object_id, self.context['acl'].pk
|
||||
), request=self.context['request'], format=self.context['format']
|
||||
)
|
||||
|
||||
|
||||
class WritableAccessControlListPermissionSerializer(AccessControlListPermissionSerializer):
|
||||
permission_pk = serializers.CharField(
|
||||
help_text=_(
|
||||
'Primary key of the new permission to grant to the access control '
|
||||
'list.'
|
||||
), write_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
fields = ('namespace',)
|
||||
read_only_fields = ('namespace',)
|
||||
read_only_fields = ('object_id',)
|
||||
|
||||
def create(self, validated_data):
|
||||
for permission in validated_data['permissions']:
|
||||
self.context['acl'].permissions.add(permission)
|
||||
role = self.get_external_object()
|
||||
|
||||
return validated_data['permissions'][0]
|
||||
if role:
|
||||
validated_data['role'] = role
|
||||
|
||||
def validate(self, attrs):
|
||||
permissions_pk_list = attrs.pop('permission_pk', None)
|
||||
permissions_result = []
|
||||
|
||||
if permissions_pk_list:
|
||||
for pk in permissions_pk_list.split(','):
|
||||
try:
|
||||
permission = Permission.get(pk=pk)
|
||||
except KeyError:
|
||||
raise ValidationError(_('No such permission: %s') % pk)
|
||||
else:
|
||||
# Accumulate valid stored permission pks
|
||||
permissions_result.append(permission.pk)
|
||||
|
||||
attrs['permissions'] = StoredPermission.objects.filter(
|
||||
pk__in=permissions_result
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
class WritableAccessControlListSerializer(serializers.ModelSerializer):
|
||||
content_type = ContentTypeSerializer(read_only=True)
|
||||
permissions_pk_list = serializers.CharField(
|
||||
help_text=_(
|
||||
'Comma separated list of permission primary keys to grant to this '
|
||||
'access control list.'
|
||||
), required=False
|
||||
)
|
||||
permissions_url = serializers.SerializerMethodField(
|
||||
help_text=_(
|
||||
'API URL pointing to the list of permissions for this access '
|
||||
'control list.'
|
||||
), read_only=True
|
||||
)
|
||||
role_pk = serializers.IntegerField(
|
||||
help_text=_(
|
||||
'Primary keys of the role to which this access control list '
|
||||
'binds to.'
|
||||
), write_only=True
|
||||
)
|
||||
url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
fields = (
|
||||
'content_type', 'id', 'object_id', 'permissions_pk_list',
|
||||
'permissions_url', 'role_pk', 'url'
|
||||
)
|
||||
model = AccessControlList
|
||||
read_only_fields = ('content_type', 'object_id')
|
||||
|
||||
def get_permissions_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-permission-list', args=(
|
||||
instance.content_type.app_label, instance.content_type.model,
|
||||
instance.object_id, instance.pk
|
||||
), request=self.context['request'], format=self.context['format']
|
||||
return super(AccessControlListSerializer, self).create(
|
||||
validated_data=validated_data
|
||||
)
|
||||
|
||||
def get_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-detail', args=(
|
||||
instance.content_type.app_label, instance.content_type.model,
|
||||
instance.object_id, instance.pk
|
||||
), request=self.context['request'], format=self.context['format']
|
||||
def update(self, instance, validated_data):
|
||||
role = self.get_external_object()
|
||||
|
||||
if role:
|
||||
validated_data['role'] = role
|
||||
|
||||
return super(AccessControlListSerializer, self).update(
|
||||
instance=instance, validated_data=validated_data
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs['content_type'] = ContentType.objects.get_for_model(
|
||||
self.context['content_object']
|
||||
)
|
||||
attrs['object_id'] = self.context['content_object'].pk
|
||||
|
||||
try:
|
||||
attrs['role'] = Role.objects.get(pk=attrs.pop('role_pk'))
|
||||
except Role.DoesNotExist as exception:
|
||||
raise ValidationError(force_text(exception))
|
||||
|
||||
permissions_pk_list = attrs.pop('permissions_pk_list', None)
|
||||
permissions_result = []
|
||||
|
||||
if permissions_pk_list:
|
||||
for pk in permissions_pk_list.split(','):
|
||||
try:
|
||||
permission = Permission.get(pk=pk)
|
||||
except KeyError:
|
||||
raise ValidationError(_('No such permission: %s') % pk)
|
||||
else:
|
||||
# Accumulate valid stored permission pks
|
||||
permissions_result.append(permission.pk)
|
||||
|
||||
instance = AccessControlList(**attrs)
|
||||
|
||||
try:
|
||||
instance.full_clean()
|
||||
except DjangoValidationError as exception:
|
||||
raise ValidationError(exception)
|
||||
|
||||
# Add a queryset of valid stored permissions so that they get added
|
||||
# after the ACL gets created.
|
||||
attrs['permissions'] = StoredPermission.objects.filter(
|
||||
pk__in=permissions_result
|
||||
)
|
||||
return attrs
|
||||
|
||||
73
mayan/apps/acls/tests/mixins.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from mayan.apps.common.tests.mixins import TestModelTestMixin
|
||||
from mayan.apps.permissions.tests.mixins import (
|
||||
PermissionTestMixin, RoleTestCaseMixin, RoleTestMixin
|
||||
)
|
||||
from mayan.apps.user_management.tests.mixins import UserTestCaseMixin
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
|
||||
class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
|
||||
def setUp(self):
|
||||
super(ACLTestCaseMixin, self).setUp()
|
||||
if hasattr(self, '_test_case_user'):
|
||||
self._test_case_role.groups.add(self._test_case_group)
|
||||
|
||||
def grant_access(self, obj, permission):
|
||||
if not hasattr(self, '_test_case_role'):
|
||||
raise ImproperlyConfigured(
|
||||
'Enable the creation of the test case user, group, and role '
|
||||
'in order to enable the usage of ACLs in tests.'
|
||||
)
|
||||
|
||||
self._test_case_acl = AccessControlList.objects.grant(
|
||||
obj=obj, permission=permission, role=self._test_case_role
|
||||
)
|
||||
|
||||
|
||||
class ACLTestMixin(PermissionTestMixin, RoleTestMixin, TestModelTestMixin):
|
||||
auto_create_test_role = True
|
||||
|
||||
def _create_test_acl(self):
|
||||
self.test_acl = AccessControlList.objects.create(
|
||||
content_object=self.test_object, role=self.test_role
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(ACLTestMixin, self).setUp()
|
||||
if self.auto_create_test_role:
|
||||
self._create_test_role()
|
||||
|
||||
def _inject_test_object_content_type(self):
|
||||
self.test_object_content_type = ContentType.objects.get_for_model(self.test_object)
|
||||
|
||||
self.test_content_object_view_kwargs = {
|
||||
'app_label': self.test_object_content_type.app_label,
|
||||
'model_name': self.test_object_content_type.model,
|
||||
'object_id': self.test_object.pk
|
||||
}
|
||||
|
||||
def _setup_test_object(self):
|
||||
self._create_test_model()
|
||||
self._create_test_object()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
permission_acl_edit, permission_acl_view,
|
||||
)
|
||||
)
|
||||
|
||||
self._create_test_permission()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
|
||||
self._inject_test_object_content_type()
|
||||
@@ -2,23 +2,20 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from document_states.tests.test_actions import ActionTestCase
|
||||
from documents.permissions import permission_document_view
|
||||
from mayan.apps.document_states.tests.test_actions import ActionTestCase
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
|
||||
from ..workflow_actions import GrantAccessAction, RevokeAccessAction
|
||||
|
||||
|
||||
class ACLActionTestCase(ActionTestCase):
|
||||
def setUp(self):
|
||||
super(ACLActionTestCase, self).setUp()
|
||||
|
||||
def test_grant_access_action(self):
|
||||
action = GrantAccessAction(
|
||||
form_data={
|
||||
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
|
||||
'object_id': self.document.pk,
|
||||
'roles': [self.role.pk],
|
||||
'permissions': [permission_document_view.uuid],
|
||||
'roles': [self._test_case_role.pk],
|
||||
'permissions': [permission_document_view.pk],
|
||||
}
|
||||
)
|
||||
action.execute(context={'entry_log': self.entry_log})
|
||||
@@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase):
|
||||
list(self.document.acls.first().permissions.all()),
|
||||
[permission_document_view.stored_permission]
|
||||
)
|
||||
self.assertEqual(self.document.acls.first().role, self.role)
|
||||
self.assertEqual(self.document.acls.first().role, self._test_case_role)
|
||||
|
||||
def test_revoke_access_action(self):
|
||||
self.grant_access(
|
||||
@@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase):
|
||||
form_data={
|
||||
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
|
||||
'object_id': self.document.pk,
|
||||
'roles': [self.role.pk],
|
||||
'permissions': [permission_document_view.uuid],
|
||||
'roles': [self._test_case_role.pk],
|
||||
'permissions': [permission_document_view.pk],
|
||||
}
|
||||
)
|
||||
action.execute(context={'entry_log': self.entry_log})
|
||||
|
||||
@@ -1,205 +1,189 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from documents.permissions import permission_document_view
|
||||
from documents.tests import DocumentTestMixin
|
||||
from permissions.tests.literals import TEST_ROLE_LABEL
|
||||
from rest_api.tests import BaseAPITestCase
|
||||
from mayan.apps.rest_api.tests import BaseAPITestCase
|
||||
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_view
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
|
||||
@override_settings(OCR_AUTO_OCR=False)
|
||||
class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
class ACLAPITestCase(ACLTestMixin, BaseAPITestCase):
|
||||
def setUp(self):
|
||||
super(ACLAPITestCase, self).setUp()
|
||||
self.login_admin_user()
|
||||
self._setup_test_object()
|
||||
self._create_test_acl()
|
||||
self.test_acl.permissions.add(self.test_permission.stored_permission)
|
||||
|
||||
self.document_content_type = ContentType.objects.get_for_model(
|
||||
self.document
|
||||
def _request_object_acl_list_api_view(self):
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-list',
|
||||
kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
|
||||
def _create_acl(self):
|
||||
self.acl = AccessControlList.objects.create(
|
||||
content_object=self.document,
|
||||
role=self.role
|
||||
)
|
||||
def test_object_acl_list_api_view_no_permission(self):
|
||||
response = self._request_object_acl_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
self.acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
def test_object_acl_list_view(self):
|
||||
self._create_acl()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-list',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk
|
||||
)
|
||||
)
|
||||
def test_object_acl_list_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['results'][0]['content_type']['app_label'],
|
||||
self.document_content_type.app_label
|
||||
self.test_object_content_type.app_label
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['results'][0]['role']['label'], TEST_ROLE_LABEL
|
||||
response.data['results'][0]['role']['label'],
|
||||
self.test_acl.role.label
|
||||
)
|
||||
|
||||
def test_object_acl_delete_view(self):
|
||||
self._create_acl()
|
||||
def _request_acl_delete_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
response = self.delete(
|
||||
viewname='rest_api:accesscontrollist-detail',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk, self.acl.pk
|
||||
)
|
||||
return self.delete(
|
||||
viewname='rest_api:object-acl-detail',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def test_object_acl_delete_api_view_with_access(self):
|
||||
self.expected_content_type = None
|
||||
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
response = self._request_acl_delete_api_view()
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||
self.assertTrue(self.test_acl not in AccessControlList.objects.all())
|
||||
|
||||
def test_object_acl_detail_view(self):
|
||||
self._create_acl()
|
||||
def test_object_acl_delete_api_view_no_permission(self):
|
||||
response = self._request_acl_delete_api_view()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-detail',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk, self.acl.pk
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertTrue(self.test_acl in AccessControlList.objects.all())
|
||||
|
||||
def _request_object_acl_detail_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-detail',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def test_object_acl_detail_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_detail_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['content_type']['app_label'],
|
||||
self.document_content_type.app_label
|
||||
self.test_object_content_type.app_label
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['role']['label'], TEST_ROLE_LABEL
|
||||
response.data['role']['label'], self.test_acl.role.label
|
||||
)
|
||||
|
||||
def test_object_acl_permission_delete_view(self):
|
||||
self._create_acl()
|
||||
permission = self.acl.permissions.first()
|
||||
def test_object_acl_detail_api_view_no_permission(self):
|
||||
response = self._request_object_acl_detail_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
response = self.delete(
|
||||
viewname='rest_api:accesscontrollist-permission-detail',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk, self.acl.pk,
|
||||
permission.pk
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(self.acl.permissions.count(), 0)
|
||||
def _request_object_acl_permission_list_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
def test_object_acl_permission_detail_view(self):
|
||||
self._create_acl()
|
||||
permission = self.acl.permissions.first()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-permission-detail',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk, self.acl.pk,
|
||||
permission.pk
|
||||
)
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-permission-list',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
response.data['pk'], permission_document_view.pk
|
||||
)
|
||||
|
||||
def test_object_acl_permission_list_view(self):
|
||||
self._create_acl()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-permission-list',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk, self.acl.pk
|
||||
)
|
||||
)
|
||||
def test_object_acl_permission_list_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['results'][0]['pk'],
|
||||
permission_document_view.pk
|
||||
self.test_permission.pk
|
||||
)
|
||||
|
||||
def test_object_acl_permission_list_post_view(self):
|
||||
self._create_acl()
|
||||
def test_object_acl_permission_list_api_view_no_permission(self):
|
||||
response = self._request_object_acl_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
response = self.post(
|
||||
viewname='rest_api:accesscontrollist-permission-list',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk, self.acl.pk
|
||||
), data={'permission_pk': permission_acl_view.pk}
|
||||
def _request_object_acl_permission_remove_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.post(
|
||||
viewname='rest_api:object-acl-permission-remove',
|
||||
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertQuerysetEqual(
|
||||
ordered=False, qs=self.acl.permissions.all(), values=(
|
||||
repr(permission_document_view.stored_permission),
|
||||
repr(permission_acl_view.stored_permission)
|
||||
)
|
||||
def test_object_acl_permission_remove_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_object_acl_permission_remove_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
|
||||
|
||||
def test_object_acl_permission_remove_api_view_no_permission(self):
|
||||
response = self._request_object_acl_permission_remove_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
|
||||
|
||||
def _request_object_acl_permission_add_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.post(
|
||||
viewname='rest_api:object-acl-permission-add',
|
||||
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
|
||||
)
|
||||
|
||||
def test_object_acl_post_no_permissions_added_view(self):
|
||||
response = self.post(
|
||||
viewname='rest_api:accesscontrollist-list',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk
|
||||
), data={'role_pk': self.role.pk}
|
||||
def test_object_acl_permission_add_api_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_object_acl_permission_add_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
|
||||
|
||||
def test_object_acl_permission_add_api_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
|
||||
response = self._request_object_acl_permission_add_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
|
||||
|
||||
def _request_object_acl_inherited_permission_list_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-permission-inherited-list',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
def test_object_acl_inherited_permission_list_api_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_inherited_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
self.document.acls.first().role, self.role
|
||||
)
|
||||
self.assertEqual(
|
||||
self.document.acls.first().content_object, self.document
|
||||
)
|
||||
self.assertEqual(
|
||||
self.document.acls.first().permissions.count(), 0
|
||||
response.data['results'][0]['pk'],
|
||||
self.test_permission.pk
|
||||
)
|
||||
|
||||
def test_object_acl_post_with_permissions_added_view(self):
|
||||
response = self.post(
|
||||
viewname='rest_api:accesscontrollist-list',
|
||||
args=(
|
||||
self.document_content_type.app_label,
|
||||
self.document_content_type.model,
|
||||
self.document.pk
|
||||
), data={
|
||||
'role_pk': self.role.pk,
|
||||
'permissions_pk_list': permission_acl_view.pk
|
||||
def test_object_acl_inherited_permission_list_api_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(
|
||||
self.document.acls.first().content_object, self.document
|
||||
)
|
||||
self.assertEqual(
|
||||
self.document.acls.first().role, self.role
|
||||
)
|
||||
self.assertEqual(
|
||||
self.document.acls.first().permissions.first(),
|
||||
permission_acl_view.stored_permission
|
||||
)
|
||||
response = self._request_object_acl_inherited_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -1,100 +1,84 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
|
||||
from documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
|
||||
from ..links import (
|
||||
link_acl_delete, link_acl_list, link_acl_create, link_acl_permissions
|
||||
link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions
|
||||
)
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
class ACLsLinksTestCase(GenericDocumentViewTestCase):
|
||||
def test_document_acl_create_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
self.login_user()
|
||||
class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
|
||||
auto_create_test_role = False
|
||||
|
||||
self.add_test_view(test_object=self.document)
|
||||
def setUp(self):
|
||||
super(AccessControlListLinksTestCase, self).setUp()
|
||||
self._setup_test_object()
|
||||
|
||||
def test_object_acl_create_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
self.add_test_view(test_object=self.test_object)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_create.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.document)
|
||||
kwargs = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse('acls:acl_create', kwargs=kwargs)
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_create',
|
||||
kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_acl_delete_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
def test_object_acl_delete_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
self.login_user()
|
||||
|
||||
self.add_test_view(test_object=acl)
|
||||
self.add_test_view(test_object=self._test_case_acl)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_delete.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse('acls:acl_delete', args=(acl.pk,))
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_delete',
|
||||
kwargs={'acl_id': self._test_case_acl.pk}
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_acl_edit_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
def test_object_acl_edit_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
self.login_user()
|
||||
|
||||
self.add_test_view(test_object=acl)
|
||||
self.add_test_view(test_object=self._test_case_acl)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_permissions.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse('acls:acl_permissions', args=(acl.pk,))
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_permissions',
|
||||
kwargs={'acl_id': self._test_case_acl.pk}
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_acl_list_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
def test_object_acl_list_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
acl.permissions.add(permission_acl_view.stored_permission)
|
||||
self.login_user()
|
||||
|
||||
self.add_test_view(test_object=self.document)
|
||||
self.add_test_view(test_object=self.test_object)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_list.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.document)
|
||||
kwargs = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse('acls:acl_list', kwargs=kwargs)
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_list',
|
||||
kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,161 +1,401 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.test import override_settings
|
||||
from django.db import models
|
||||
|
||||
from common.tests import BaseTestCase
|
||||
from documents.models import Document, DocumentType
|
||||
from documents.permissions import permission_document_view
|
||||
from documents.tests import (
|
||||
TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE_LABEL,
|
||||
TEST_DOCUMENT_TYPE_2_LABEL
|
||||
from mayan.apps.common.tests import BaseTestCase
|
||||
from mayan.apps.documents.models import Document, DocumentType
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.documents.tests import (
|
||||
DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
|
||||
)
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
|
||||
class PermissionTestCase(DocumentTestMixin, BaseTestCase):
|
||||
auto_create_document_type = False
|
||||
|
||||
@override_settings(OCR_AUTO_OCR=False)
|
||||
class PermissionTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(PermissionTestCase, self).setUp()
|
||||
self.document_type_1 = DocumentType.objects.create(
|
||||
self.test_document_type_1 = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE_LABEL
|
||||
)
|
||||
|
||||
self.document_type_2 = DocumentType.objects.create(
|
||||
self.test_document_type_2 = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE_2_LABEL
|
||||
)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document_1 = self.document_type_1.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document_2 = self.document_type_1.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document_3 = self.document_type_2.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for document_type in DocumentType.objects.all():
|
||||
document_type.delete()
|
||||
super(PermissionTestCase, self).tearDown()
|
||||
self.test_document_1 = self.upload_document(
|
||||
document_type=self.test_document_type_1
|
||||
)
|
||||
self.test_document_2 = self.upload_document(
|
||||
document_type=self.test_document_type_1
|
||||
)
|
||||
self.test_document_3 = self.upload_document(
|
||||
document_type=self.test_document_type_2
|
||||
)
|
||||
|
||||
def test_check_access_without_permissions(self):
|
||||
with self.assertRaises(PermissionDenied):
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,),
|
||||
user=self.user, obj=self.document_1
|
||||
obj=self.test_document_1, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
|
||||
def test_filtering_without_permissions(self):
|
||||
self.assertQuerysetEqual(
|
||||
AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
), []
|
||||
self.assertEqual(
|
||||
AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view,
|
||||
queryset=Document.objects.all(), user=self._test_case_user,
|
||||
).count(), 0
|
||||
)
|
||||
|
||||
def test_check_access_with_acl(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_1, role=self.role
|
||||
content_object=self.test_document_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,), user=self.user,
|
||||
obj=self.document_1
|
||||
obj=self.test_document_1, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
def test_filtering_with_permissions(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_1, role=self.role
|
||||
content_object=self.test_document_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
), (repr(self.document_1),)
|
||||
AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view,
|
||||
queryset=Document.objects.all(), user=self._test_case_user
|
||||
), (repr(self.test_document_1),)
|
||||
)
|
||||
|
||||
def test_check_access_with_inherited_acl(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,), user=self.user,
|
||||
obj=self.document_1
|
||||
obj=self.test_document_1, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
def test_check_access_with_inherited_acl_and_local_acl(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
def test_check_access_with_inherited_acl_and_direct_acl(self):
|
||||
test_acl_1 = AccessControlList.objects.create(
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
test_acl_1.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_3, role=self.role
|
||||
test_acl_2 = AccessControlList.objects.create(
|
||||
content_object=self.test_document_3, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
test_acl_2.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,), user=self.user,
|
||||
obj=self.document_3
|
||||
obj=self.test_document_3, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
def test_filtering_with_inherited_permissions(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
result = AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
result = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, queryset=Document.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
|
||||
# Since document_1 and document_2 are of document_type_1
|
||||
# they are the only ones that should be returned
|
||||
|
||||
self.assertTrue(self.document_1 in result)
|
||||
self.assertTrue(self.document_2 in result)
|
||||
self.assertTrue(self.document_3 not in result)
|
||||
self.assertTrue(self.test_document_1 in result)
|
||||
self.assertTrue(self.test_document_2 in result)
|
||||
self.assertTrue(self.test_document_3 not in result)
|
||||
|
||||
def test_filtering_with_inherited_permissions_and_local_acl(self):
|
||||
self.role.permissions.add(permission_document_view.stored_permission)
|
||||
self._test_case_role.permissions.add(
|
||||
permission_document_view.stored_permission
|
||||
)
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_3, role=self.role
|
||||
content_object=self.test_document_3, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
result = AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
result = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, queryset=Document.objects.all(),
|
||||
user=self._test_case_user,
|
||||
)
|
||||
self.assertTrue(self.document_1 in result)
|
||||
self.assertTrue(self.document_2 in result)
|
||||
self.assertTrue(self.document_3 in result)
|
||||
self.assertTrue(self.test_document_1 in result)
|
||||
self.assertTrue(self.test_document_2 in result)
|
||||
self.assertTrue(self.test_document_3 in result)
|
||||
|
||||
|
||||
class InheritedPermissionTestCase(ACLTestMixin, BaseTestCase):
|
||||
def test_retrieve_inherited_role_permission_not_model_applicable(self):
|
||||
self._create_test_model()
|
||||
self.test_object = self.TestModel.objects.create()
|
||||
self._create_test_acl()
|
||||
self._create_test_permission()
|
||||
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=self.test_object, role=self.test_role
|
||||
)
|
||||
self.assertTrue(self.test_permission.stored_permission not in queryset)
|
||||
|
||||
def test_retrieve_inherited_role_permission_model_applicable(self):
|
||||
self._create_test_model()
|
||||
self.test_object = self.TestModel.objects.create()
|
||||
self._create_test_acl()
|
||||
self._create_test_permission()
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=self.test_object, role=self.test_role
|
||||
)
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
def test_retrieve_inherited_related_parent_child_permission(self):
|
||||
self._create_test_permission()
|
||||
|
||||
self._create_test_model(model_name='TestModelParent')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children',
|
||||
to='TestModelParent',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelChild, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent',
|
||||
)
|
||||
|
||||
parent = self.TestModelParent.objects.create()
|
||||
child = self.TestModelChild.objects.create(parent=parent)
|
||||
|
||||
AccessControlList.objects.grant(
|
||||
obj=parent, permission=self.test_permission, role=self.test_role
|
||||
)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=child, role=self.test_role
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
def test_retrieve_inherited_related_grandparent_parent_child_permission(self):
|
||||
self._create_test_permission()
|
||||
|
||||
self._create_test_model(model_name='TestModelGrandParent')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children',
|
||||
to='TestModelGrandParent',
|
||||
)
|
||||
}, model_name='TestModelParent'
|
||||
)
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children',
|
||||
to='TestModelParent',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelGrandParent, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelChild, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent',
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelParent, related='parent',
|
||||
)
|
||||
|
||||
grandparent = self.TestModelGrandParent.objects.create()
|
||||
parent = self.TestModelParent.objects.create(parent=grandparent)
|
||||
child = self.TestModelChild.objects.create(parent=parent)
|
||||
|
||||
AccessControlList.objects.grant(
|
||||
obj=grandparent, permission=self.test_permission,
|
||||
role=self.test_role
|
||||
)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=child, role=self.test_role
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
|
||||
class MultipleAccessTestCase(ACLTestMixin, BaseTestCase):
|
||||
def setUp(self):
|
||||
super(MultipleAccessTestCase, self).setUp()
|
||||
self._create_test_permission()
|
||||
self._create_test_permission_2()
|
||||
|
||||
self._create_test_model(model_name='TestModelParent1')
|
||||
self._create_test_model(model_name='TestModelParent2')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent_1': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children1',
|
||||
to='TestModelParent1',
|
||||
),
|
||||
'parent_2': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children2',
|
||||
to='TestModelParent2',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent1, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent2, permissions=(
|
||||
self.test_permission_2,
|
||||
)
|
||||
)
|
||||
|
||||
self.test_object_parent_1 = self.TestModelParent1.objects.create()
|
||||
self.test_object_parent_2 = self.TestModelParent2.objects.create()
|
||||
self.test_object_child = self.TestModelChild.objects.create(
|
||||
parent_1=self.test_object_parent_1, parent_2=self.test_object_parent_2
|
||||
)
|
||||
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent_1'
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent_2'
|
||||
)
|
||||
|
||||
def test_restrict_queryset_and_operator_first_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child not in queryset)
|
||||
|
||||
def test_restrict_queryset_and_operator_second_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child not in queryset)
|
||||
|
||||
def test_restrict_queryset_and_operator_both_permissions(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_first_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_second_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_both_permissions(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
@@ -1,191 +1,239 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
class AccessControlListViewTestCase(GenericDocumentViewTestCase):
|
||||
|
||||
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
|
||||
def setUp(self):
|
||||
super(AccessControlListViewTestCase, self).setUp()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.document)
|
||||
self._create_test_model()
|
||||
self._create_test_object()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
permission_acl_edit, permission_acl_view,
|
||||
)
|
||||
)
|
||||
|
||||
self.view_arguments = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}
|
||||
self._create_test_permission()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
|
||||
def test_acl_create_view_no_permission(self):
|
||||
self.login_user()
|
||||
self._inject_test_object_content_type()
|
||||
|
||||
response = self.get(
|
||||
viewname='acls:acl_create', kwargs=self.view_arguments, data={
|
||||
'role': self.role.pk
|
||||
self._create_test_acl()
|
||||
self.test_acl.permissions.add(self.test_permission.stored_permission)
|
||||
|
||||
def _request_acl_create_get_view(self):
|
||||
return self.get(
|
||||
viewname='acls:acl_create',
|
||||
kwargs=self.test_content_object_view_kwargs, data={
|
||||
'role': self.test_role.pk
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||
def test_acl_create_get_view_no_permission(self):
|
||||
self.test_acl.delete()
|
||||
|
||||
def test_acl_create_view_with_permission(self):
|
||||
self.login_user()
|
||||
response = self._request_acl_create_get_view()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_acl_edit.stored_permission
|
||||
)
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
response = self.get(
|
||||
viewname='acls:acl_create', kwargs=self.view_arguments, data={
|
||||
'role': self.role.pk
|
||||
}, follow=True
|
||||
)
|
||||
def test_acl_create_get_view_with_object_access(self):
|
||||
self.test_acl.delete()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_acl_create_get_view()
|
||||
self.assertContains(
|
||||
response, text=self.document.label, status_code=200
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def _request_acl_create_post_view(self):
|
||||
return self.post(
|
||||
viewname='acls:acl_create',
|
||||
kwargs=self.test_content_object_view_kwargs, data={
|
||||
'role': self.test_role.pk
|
||||
}
|
||||
)
|
||||
|
||||
def test_acl_create_view_post_no_permission(self):
|
||||
self.login_user()
|
||||
self.test_acl.delete()
|
||||
|
||||
response = self.post(
|
||||
viewname='acls:acl_create', kwargs=self.view_arguments, data={
|
||||
'role': self.role.pk
|
||||
}
|
||||
)
|
||||
response = self._request_acl_create_post_view()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def test_acl_create_view_with_post_permission(self):
|
||||
self.login_user()
|
||||
def test_acl_create_view_post_with_access(self):
|
||||
self.test_acl.delete()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_acl_edit.stored_permission
|
||||
)
|
||||
response = self._request_acl_create_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
response = self.post(
|
||||
viewname='acls:acl_create', kwargs=self.view_arguments, data={
|
||||
'role': self.role.pk
|
||||
}, follow=True
|
||||
)
|
||||
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
self.assertContains(response, text='created', status_code=200)
|
||||
self.assertEqual(AccessControlList.objects.count(), 1)
|
||||
|
||||
def test_acl_create_duplicate_view_with_permission(self):
|
||||
def test_acl_create_duplicate_view_with_access(self):
|
||||
"""
|
||||
Test creating a duplicate ACL entry: same object & role
|
||||
Result: Should redirect to existing ACL for object + role combination
|
||||
"""
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_acl_create_post_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_acl.role),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
self.login_user()
|
||||
# 2 ACLs: 1 created by the test and the other by the self.grant_access
|
||||
self.assertEqual(AccessControlList.objects.count(), 2)
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_acl_edit.stored_permission
|
||||
# Sorted by role PK
|
||||
expected_results = sorted(
|
||||
[
|
||||
{
|
||||
# Test role, created and then requested,
|
||||
# but created only once
|
||||
'object_id': self.test_object.pk,
|
||||
'role': self.test_role.pk
|
||||
},
|
||||
{
|
||||
# Test case ACL for the test case role, ignored
|
||||
'object_id': self.test_object.pk,
|
||||
'role': self._test_case_role.pk
|
||||
},
|
||||
], key=lambda item: item['role']
|
||||
)
|
||||
|
||||
response = self.post(
|
||||
viewname='acls:acl_create', kwargs=self.view_arguments, data={
|
||||
'role': self.role.pk
|
||||
}, follow=True
|
||||
self.assertQuerysetEqual(
|
||||
qs=AccessControlList.objects.order_by('role__id').values(
|
||||
'object_id', 'role',
|
||||
), transform=dict, values=expected_results
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, text='vailable permissions', status_code=200
|
||||
)
|
||||
self.assertEqual(AccessControlList.objects.count(), 1)
|
||||
self.assertEqual(AccessControlList.objects.first().pk, acl.pk)
|
||||
|
||||
def test_orphan_acl_create_view_with_permission(self):
|
||||
"""
|
||||
Test creating an ACL entry for an object with no model permissions.
|
||||
Result: Should display a blank permissions list (not optgroup)
|
||||
"""
|
||||
self.login_user()
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_acl_edit.stored_permission
|
||||
def _request_acl_delete_view(self):
|
||||
return self.post(
|
||||
viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk}
|
||||
)
|
||||
|
||||
recent_entry = self.document.add_as_recent_document_for_user(self.user)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(recent_entry)
|
||||
|
||||
view_arguments = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': recent_entry.pk
|
||||
}
|
||||
|
||||
response = self.post(
|
||||
viewname='acls:acl_create', kwargs=view_arguments, data={
|
||||
'role': self.role.pk
|
||||
}, follow=True
|
||||
def test_acl_delete_view_no_permission(self):
|
||||
response = self._request_acl_delete_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
|
||||
self.assertNotContains(response, text='optgroup', status_code=200)
|
||||
self.assertEqual(AccessControlList.objects.count(), 1)
|
||||
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def test_acl_delete_view_with_access(self):
|
||||
self.grant_access(
|
||||
obj=self.test_object, permission=permission_acl_edit
|
||||
)
|
||||
|
||||
response = self._request_acl_delete_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def _request_acl_list_view(self):
|
||||
return self.get(
|
||||
viewname='acls:acl_list', kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
|
||||
def test_acl_list_view_no_permission(self):
|
||||
self.login_user()
|
||||
response = self._request_acl_list_view()
|
||||
|
||||
document = self.document.add_as_recent_document_for_user(
|
||||
self.user
|
||||
).document
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=document, role=self.role
|
||||
)
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(document)
|
||||
|
||||
view_arguments = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': document.pk
|
||||
}
|
||||
|
||||
response = self.get(
|
||||
viewname='acls:acl_list', kwargs=view_arguments
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
|
||||
self.assertNotContains(response, text=document.label, status_code=403)
|
||||
self.assertNotContains(response, text='otal: 1', status_code=403)
|
||||
def test_acl_list_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
def test_acl_list_view_with_permission(self):
|
||||
self.login_user()
|
||||
response = self._request_acl_list_view()
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_acl_view.stored_permission
|
||||
self.assertContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
document = self.document.add_as_recent_document_for_user(
|
||||
self.user
|
||||
).document
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=document, role=self.role
|
||||
def _request_get_acl_permissions_get_view(self):
|
||||
return self.get(
|
||||
viewname='acls:acl_permissions',
|
||||
kwargs={'acl_id': self.test_acl.pk}
|
||||
)
|
||||
acl.permissions.add(permission_acl_view.stored_permission)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(document)
|
||||
def test_acl_permissions_get_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
|
||||
view_arguments = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': document.pk
|
||||
}
|
||||
|
||||
response = self.get(
|
||||
viewname='acls:acl_list', kwargs=view_arguments
|
||||
response = self._request_get_acl_permissions_get_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
def test_acl_permissions_get_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_get_acl_permissions_get_view()
|
||||
self.assertContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
def _request_post_acl_permissions_post_view(self):
|
||||
return self.post(
|
||||
viewname='acls:acl_permissions',
|
||||
kwargs={'acl_id': self.test_acl.pk},
|
||||
data={'available-selection': self.test_permission.stored_permission.pk}
|
||||
)
|
||||
|
||||
def test_acl_permissions_post_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
|
||||
response = self._request_post_acl_permissions_post_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
def test_acl_permissions_post_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_post_acl_permissions_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertTrue(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
self.assertContains(response, text=document.label, status_code=200)
|
||||
|
||||
@@ -2,45 +2,33 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .api_views import (
|
||||
APIObjectACLListView, APIObjectACLPermissionListView,
|
||||
APIObjectACLPermissionView, APIObjectACLView
|
||||
)
|
||||
from .api_views import ObjectACLAPIViewSet
|
||||
from .views import (
|
||||
ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/create/$',
|
||||
ACLCreateView.as_view(), name='acl_create'
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model_name>[-\w]+)/(?P<object_id>\d+)/create/$',
|
||||
name='acl_create', view=ACLCreateView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$',
|
||||
ACLListView.as_view(), name='acl_list'
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model_name>[-\w]+)/(?P<object_id>\d+)/list/$',
|
||||
name='acl_list', view=ACLListView.as_view()
|
||||
),
|
||||
url(r'^(?P<pk>\d+)/delete/$', ACLDeleteView.as_view(), name='acl_delete'),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/permissions/$', ACLPermissionsView.as_view(),
|
||||
name='acl_permissions'
|
||||
regex=r'^acls/(?P<acl_id>\d+)/delete/$', name='acl_delete',
|
||||
view=ACLDeleteView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^acls/(?P<acl_id>\d+)/permissions/$', name='acl_permissions',
|
||||
view=ACLPermissionsView.as_view()
|
||||
),
|
||||
]
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$',
|
||||
APIObjectACLListView.as_view(), name='accesscontrollist-list'
|
||||
),
|
||||
url(
|
||||
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$',
|
||||
APIObjectACLView.as_view(), name='accesscontrollist-detail'
|
||||
),
|
||||
url(
|
||||
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$',
|
||||
APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list'
|
||||
),
|
||||
url(
|
||||
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$',
|
||||
APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail'
|
||||
),
|
||||
]
|
||||
api_router_entries = (
|
||||
{
|
||||
'prefix': r'apps/(?P<app_label>[^/.]+)/models/(?P<model_name>[^/.]+)/objects/(?P<object_id>[^/.]+)/acls',
|
||||
'viewset': ObjectACLAPIViewSet, 'basename': 'object-acl'
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.views import (
|
||||
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
|
||||
from mayan.apps.common.mixins import (
|
||||
ContentTypeViewMixin, ExternalObjectMixin
|
||||
)
|
||||
from mayan.apps.common.generics import (
|
||||
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
|
||||
SingleObjectListView
|
||||
)
|
||||
from permissions import PermissionNamespace, Permission
|
||||
from permissions.models import StoredPermission
|
||||
from mayan.apps.permissions.models import Role
|
||||
|
||||
from .classes import ModelPermission
|
||||
from .forms import ACLCreateForm
|
||||
from .icons import icon_acl_list
|
||||
from .links import link_acl_create
|
||||
from .models import AccessControlList
|
||||
@@ -27,113 +26,95 @@ from .permissions import permission_acl_edit, permission_acl_view
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ACLCreateView(SingleObjectCreateView):
|
||||
fields = ('role',)
|
||||
model = AccessControlList
|
||||
class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView):
|
||||
content_type_url_kw_args = {
|
||||
'app_label': 'app_label',
|
||||
'model': 'model_name'
|
||||
}
|
||||
external_object_permission = permission_acl_edit
|
||||
external_object_pk_url_kwarg = 'object_id'
|
||||
form_class = ACLCreateForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object_content_type = get_object_or_404(
|
||||
ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
def get_error_message_duplicate(self):
|
||||
return _(
|
||||
'An ACL for "%(object)s" using role "%(role)s" already exists. '
|
||||
'Edit that ACL entry instead.'
|
||||
) % {'object': self.get_external_object(), 'role': self.object.role}
|
||||
|
||||
try:
|
||||
self.content_object = self.object_content_type.get_object_for_this_type(
|
||||
pk=self.kwargs['object_id']
|
||||
)
|
||||
except self.object_content_type.model_class().DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_edit, user=request.user,
|
||||
obj=self.content_object
|
||||
)
|
||||
|
||||
return super(ACLCreateView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_instance_extra_data(self):
|
||||
return {
|
||||
'content_object': self.content_object
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
acl = AccessControlList.objects.get(
|
||||
content_type=self.object_content_type,
|
||||
object_id=self.content_object.pk,
|
||||
role=form.cleaned_data['role']
|
||||
)
|
||||
except AccessControlList.DoesNotExist:
|
||||
return super(ACLCreateView, self).form_valid(form)
|
||||
else:
|
||||
return HttpResponseRedirect(
|
||||
reverse('acls:acl_permissions', args=(acl.pk,))
|
||||
)
|
||||
def get_external_object_queryset(self):
|
||||
# Here we get a queryset the object model for which an ACL will be
|
||||
# created.
|
||||
return self.get_content_type().get_all_objects_for_this_type()
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'object': self.content_object,
|
||||
'object': self.get_external_object(),
|
||||
'title': _(
|
||||
'New access control lists for: %s'
|
||||
) % self.content_object
|
||||
) % self.get_external_object()
|
||||
}
|
||||
|
||||
def get_form_extra_kwargs(self):
|
||||
return {
|
||||
'field_name': 'role',
|
||||
'label': _('Role'),
|
||||
'queryset': Role.objects.exclude(
|
||||
pk__in=self.get_external_object().acls.values('role')
|
||||
),
|
||||
'widget_attributes': {'class': 'select2'},
|
||||
'user': self.request.user
|
||||
}
|
||||
|
||||
def get_instance_extra_data(self):
|
||||
return {
|
||||
'content_object': self.get_external_object()
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
self.get_external_object().acls.all()
|
||||
|
||||
def get_success_url(self):
|
||||
if self.object.pk:
|
||||
return reverse('acls:acl_permissions', args=(self.object.pk,))
|
||||
else:
|
||||
return super(ACLCreateView, self).get_success_url()
|
||||
return self.object.get_absolute_url()
|
||||
|
||||
|
||||
class ACLDeleteView(SingleObjectDeleteView):
|
||||
model = AccessControlList
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_edit, user=request.user,
|
||||
obj=acl.content_object
|
||||
)
|
||||
|
||||
return super(ACLDeleteView, self).dispatch(request, *args, **kwargs)
|
||||
object_permission = permission_acl_edit
|
||||
pk_url_kwarg = 'acl_id'
|
||||
|
||||
def get_extra_context(self):
|
||||
acl = self.get_object()
|
||||
|
||||
return {
|
||||
'object': self.get_object().content_object,
|
||||
'acl': acl,
|
||||
'object': acl.content_object,
|
||||
'navigation_object_list': ('object', 'acl'),
|
||||
'title': _('Delete ACL: %s') % self.get_object(),
|
||||
}
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
instance = self.get_object()
|
||||
return reverse(
|
||||
'acls:acl_list', args=(
|
||||
instance.content_type.app_label,
|
||||
instance.content_type.model, instance.object_id
|
||||
)
|
||||
'acls:acl_list', kwargs={
|
||||
'app_label': instance.content_type.app_label,
|
||||
'model_name': instance.content_type.model,
|
||||
'object_id': instance.object_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ACLListView(SingleObjectListView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object_content_type = get_object_or_404(
|
||||
ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
|
||||
content_type_url_kw_args = {
|
||||
'app_label': 'app_label',
|
||||
'model': 'model_name'
|
||||
}
|
||||
external_object_permission = permission_acl_view
|
||||
external_object_pk_url_kwarg = 'object_id'
|
||||
|
||||
try:
|
||||
self.content_object = self.object_content_type.get_object_for_this_type(
|
||||
pk=self.kwargs['object_id']
|
||||
)
|
||||
except self.object_content_type.model_class().DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_view, user=request.user,
|
||||
obj=self.content_object
|
||||
)
|
||||
|
||||
return super(ACLListView, self).dispatch(request, *args, **kwargs)
|
||||
def get_external_object_queryset(self):
|
||||
# Here we get a queryset the object model for which an ACL will be
|
||||
# created.
|
||||
return self.get_content_type().get_all_objects_for_this_type()
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
@@ -141,7 +122,9 @@ class ACLListView(SingleObjectListView):
|
||||
'no_results_icon': icon_acl_list,
|
||||
'no_results_main_link': link_acl_create.resolve(
|
||||
context=RequestContext(
|
||||
self.request, {'resolved_object': self.content_object}
|
||||
self.request, {
|
||||
'resolved_object': self.get_external_object()
|
||||
}
|
||||
)
|
||||
),
|
||||
'no_results_title': _(
|
||||
@@ -149,116 +132,98 @@ class ACLListView(SingleObjectListView):
|
||||
),
|
||||
'no_results_text': _(
|
||||
'ACL stands for Access Control List and is a precise method '
|
||||
' to control user access to objects in the system.'
|
||||
' to control user access to objects in the system. ACLs '
|
||||
'allow granting a permission to a role but only for a '
|
||||
'specific object or set of objects.'
|
||||
),
|
||||
'object': self.get_external_object(),
|
||||
'title': _(
|
||||
'Access control lists for: %s' % self.get_external_object()
|
||||
),
|
||||
'object': self.content_object,
|
||||
'title': _('Access control lists for: %s' % self.content_object),
|
||||
}
|
||||
|
||||
def get_object_list(self):
|
||||
return AccessControlList.objects.filter(
|
||||
content_type=self.object_content_type,
|
||||
object_id=self.content_object.pk
|
||||
def get_source_queryset(self):
|
||||
return self.get_external_object().acls.all()
|
||||
|
||||
|
||||
class ACLPermissionsView(AddRemoveView):
|
||||
action_add_method = 'permissions_add'
|
||||
action_remove_method = 'permissions_remove'
|
||||
main_object_model = AccessControlList
|
||||
main_object_permission = permission_acl_edit
|
||||
main_object_pk_url_kwarg = 'acl_id'
|
||||
list_added_title = _('Granted permissions')
|
||||
list_available_title = _('Available permissions')
|
||||
related_field = 'permissions'
|
||||
|
||||
def generate_choices(self, queryset):
|
||||
namespaces_dictionary = {}
|
||||
|
||||
# Sort permissions by their translatable label
|
||||
object_list = sorted(
|
||||
queryset, key=lambda permission: permission.volatile_permission.label
|
||||
)
|
||||
|
||||
|
||||
class ACLPermissionsView(AssignRemoveView):
|
||||
grouped = True
|
||||
left_list_title = _('Available permissions')
|
||||
right_list_title = _('Granted permissions')
|
||||
|
||||
@staticmethod
|
||||
def generate_choices(entries):
|
||||
results = []
|
||||
|
||||
entries = sorted(
|
||||
entries, key=lambda x: (
|
||||
x.get_volatile_permission().namespace.label,
|
||||
x.get_volatile_permission().label
|
||||
# Group permissions by namespace
|
||||
for permission in object_list:
|
||||
namespaces_dictionary.setdefault(
|
||||
permission.volatile_permission.namespace.label,
|
||||
[]
|
||||
)
|
||||
)
|
||||
|
||||
for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace):
|
||||
permission_options = [
|
||||
(force_text(permission.pk), permission) for permission in permissions
|
||||
]
|
||||
results.append(
|
||||
(PermissionNamespace.get(namespace), permission_options)
|
||||
namespaces_dictionary[permission.volatile_permission.namespace.label].append(
|
||||
(permission.pk, force_text(permission))
|
||||
)
|
||||
|
||||
return results
|
||||
# Sort permissions by their translatable namespace label
|
||||
return sorted(namespaces_dictionary.items())
|
||||
|
||||
def add(self, item):
|
||||
permission = get_object_or_404(StoredPermission, pk=item)
|
||||
self.get_object().permissions.add(permission)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
acl = get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_edit, user=request.user,
|
||||
obj=acl.content_object
|
||||
)
|
||||
|
||||
return super(
|
||||
ACLPermissionsView, self
|
||||
).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_available_list(self):
|
||||
return ModelPermission.get_for_instance(
|
||||
instance=self.get_object().content_object
|
||||
).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
|
||||
def get_actions_extra_kwargs(self):
|
||||
return {'_user': self.request.user}
|
||||
|
||||
def get_disabled_choices(self):
|
||||
"""
|
||||
Get permissions from a parent's acls but remove the permissions we
|
||||
already hold for this object
|
||||
Get permissions from a parent's ACLs or directly granted to the role.
|
||||
We return a list since that is what the form widget's can process.
|
||||
"""
|
||||
return map(
|
||||
str, set(
|
||||
self.get_object().get_inherited_permissions().values_list(
|
||||
'pk', flat=True
|
||||
)
|
||||
).difference(
|
||||
self.get_object().permissions.values_list('pk', flat=True)
|
||||
)
|
||||
)
|
||||
return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'object': self.get_object().content_object,
|
||||
'title': _('Role "%(role)s" permission\'s for "%(object)s"') % {
|
||||
'role': self.get_object().role,
|
||||
'object': self.get_object().content_object,
|
||||
},
|
||||
'acl': self.main_object,
|
||||
'object': self.main_object.content_object,
|
||||
'navigation_object_list': ('object', 'acl'),
|
||||
'title': _('Role "%(role)s" permission\'s for "%(object)s".') % {
|
||||
'role': self.main_object.role,
|
||||
'object': self.main_object.content_object,
|
||||
}
|
||||
}
|
||||
|
||||
def get_granted_list(self):
|
||||
"""
|
||||
Merge or permissions we hold for this object and the permissions we
|
||||
hold for this object's parent via another ACL
|
||||
"""
|
||||
merged_pks = self.get_object().permissions.values_list('pk', flat=True) | self.get_object().get_inherited_permissions().values_list('pk', flat=True)
|
||||
return StoredPermission.objects.filter(pk__in=merged_pks)
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(AccessControlList, pk=self.kwargs['pk'])
|
||||
|
||||
def get_right_list_help_text(self):
|
||||
if self.get_object().get_inherited_permissions():
|
||||
def get_list_added_help_text(self):
|
||||
if self.main_object.get_inherited_permissions():
|
||||
return _(
|
||||
'Disabled permissions are inherited from a parent object.'
|
||||
'Disabled permissions are inherited from a parent object or '
|
||||
'directly granted to the role and can\'t be removed from this '
|
||||
'view. Inherited permissions need to be removed from the '
|
||||
'parent object\'s ACL or from them role via the Setup menu.'
|
||||
)
|
||||
|
||||
return None
|
||||
def get_list_added_queryset(self):
|
||||
"""
|
||||
Merge of permissions we hold for this object and the permissions we
|
||||
hold for this object's parents via another ACL. .distinct() is added
|
||||
in case the permission was added to the ACL and then added to a
|
||||
parent ACL's and thus inherited and would appear twice. If
|
||||
order to remove the double permission from the ACL it would need to be
|
||||
remove from the parent first to enable the choice in the form,
|
||||
remove it from the ACL and then re-add it to the parent ACL.
|
||||
"""
|
||||
queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
|
||||
|
||||
def left_list(self):
|
||||
Permission.refresh()
|
||||
return ACLPermissionsView.generate_choices(self.get_available_list())
|
||||
return (
|
||||
queryset_acl | self.main_object.get_inherited_permissions()
|
||||
).distinct()
|
||||
|
||||
def remove(self, item):
|
||||
permission = get_object_or_404(StoredPermission, pk=item)
|
||||
self.get_object().permissions.remove(permission)
|
||||
|
||||
def right_list(self):
|
||||
return ACLPermissionsView.generate_choices(self.get_granted_list())
|
||||
def get_secondary_object_source_queryset(self):
|
||||
return ModelPermission.get_for_instance(
|
||||
instance=self.main_object.content_object
|
||||
)
|
||||
|
||||
@@ -7,10 +7,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from acls.models import AccessControlList
|
||||
from document_states.classes import WorkflowAction
|
||||
from permissions.classes import Permission
|
||||
from permissions.models import Role
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.document_states.classes import WorkflowAction
|
||||
from mayan.apps.permissions.classes import Permission
|
||||
from mayan.apps.permissions.models import Role
|
||||
|
||||
from .classes import ModelPermission
|
||||
from .permissions import permission_acl_edit
|
||||
@@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction):
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_edit, user=request.user, obj=obj
|
||||
obj=obj, permissions=permission_acl_edit,
|
||||
user=request.user
|
||||
)
|
||||
except Exception as exception:
|
||||
raise ValidationError(exception)
|
||||
@@ -98,7 +99,9 @@ class GrantAccessAction(WorkflowAction):
|
||||
|
||||
def get_form_schema(self, *args, **kwargs):
|
||||
self.fields['content_type']['kwargs']['queryset'] = ModelPermission.get_classes(as_content_type=True)
|
||||
self.fields['permissions']['kwargs']['choices'] = Permission.all(as_choices=True)
|
||||
self.fields['permissions']['kwargs']['choices'] = Permission.all(
|
||||
as_choices=True
|
||||
)
|
||||
return super(GrantAccessAction, self).get_form_schema(*args, **kwargs)
|
||||
|
||||
def get_execute_data(self):
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
default_app_config = 'appearance.apps.AppearanceApp'
|
||||
default_app_config = 'mayan.apps.appearance.apps.AppearanceApp'
|
||||
|
||||
@@ -2,13 +2,13 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common import MayanAppConfig
|
||||
from mayan.apps.common import MayanAppConfig
|
||||
|
||||
from .licenses import * # NOQA
|
||||
|
||||
|
||||
class AppearanceApp(MayanAppConfig):
|
||||
name = 'appearance'
|
||||
name = 'mayan.apps.appearance'
|
||||
verbose_name = _('Appearance')
|
||||
|
||||
def ready(self):
|
||||
|
||||
@@ -23,11 +23,38 @@ class FontAwesomeDriver(IconDriver):
|
||||
self.symbol = symbol
|
||||
|
||||
def render(self):
|
||||
return get_template(self.template_name).render(
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'symbol': self.symbol}
|
||||
)
|
||||
|
||||
|
||||
class FontAwesomeDualDriver(IconDriver):
|
||||
name = 'fontawesome-dual'
|
||||
template_name = 'appearance/icons/font_awesome_layers.html'
|
||||
|
||||
def __init__(self, primary_symbol, secondary_symbol):
|
||||
self.primary_symbol = primary_symbol
|
||||
self.secondary_symbol = secondary_symbol
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={
|
||||
'data': (
|
||||
{
|
||||
'class': 'fas fa-circle',
|
||||
'transform': 'down-3 right-10',
|
||||
'mask': 'fas fa-{}'.format(self.primary_symbol)
|
||||
},
|
||||
{'class': 'far fa-circle', 'transform': 'down-3 right-10'},
|
||||
{
|
||||
'class': 'fas fa-{}'.format(self.secondary_symbol),
|
||||
'transform': 'shrink-4 down-3 right-10'
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FontAwesomeCSSDriver(IconDriver):
|
||||
name = 'fontawesomecss'
|
||||
template_name = 'appearance/icons/font_awesome_css.html'
|
||||
@@ -36,11 +63,37 @@ class FontAwesomeCSSDriver(IconDriver):
|
||||
self.css_classes = css_classes
|
||||
|
||||
def render(self):
|
||||
return get_template(self.template_name).render(
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'css_classes': self.css_classes}
|
||||
)
|
||||
|
||||
|
||||
class FontAwesomeMasksDriver(IconDriver):
|
||||
name = 'fontawesome-masks'
|
||||
template_name = 'appearance/icons/font_awesome_masks.html'
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'data': self.data}
|
||||
)
|
||||
|
||||
|
||||
class FontAwesomeLayersDriver(IconDriver):
|
||||
name = 'fontawesome-layers'
|
||||
template_name = 'appearance/icons/font_awesome_layers.html'
|
||||
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def render(self):
|
||||
return get_template(template_name=self.template_name).render(
|
||||
context={'data': self.data}
|
||||
)
|
||||
|
||||
|
||||
class Icon(object):
|
||||
def __init__(self, driver_name, **kwargs):
|
||||
self.driver = IconDriver.get(name=driver_name)(**kwargs)
|
||||
@@ -49,5 +102,8 @@ class Icon(object):
|
||||
return self.driver.render(**kwargs)
|
||||
|
||||
|
||||
IconDriver.register(driver_class=FontAwesomeDriver)
|
||||
IconDriver.register(driver_class=FontAwesomeCSSDriver)
|
||||
IconDriver.register(driver_class=FontAwesomeDriver)
|
||||
IconDriver.register(driver_class=FontAwesomeDualDriver)
|
||||
IconDriver.register(driver_class=FontAwesomeLayersDriver)
|
||||
IconDriver.register(driver_class=FontAwesomeMasksDriver)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from common.classes import Package
|
||||
from mayan.apps.common.classes import Package
|
||||
|
||||
Package(label='Bootstrap', license_text='''
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -1 +1 @@
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 80
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 120
|
||||
|
||||
@@ -2,11 +2,12 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from smart_settings import Namespace
|
||||
from mayan.apps.smart_settings import Namespace
|
||||
|
||||
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
|
||||
|
||||
namespace = Namespace(name='appearance', label=_('Appearance'))
|
||||
namespace = Namespace(label=_('Appearance'), name='appearance')
|
||||
|
||||
setting_max_title_length = namespace.add_setting(
|
||||
default=DEFAULT_MAXIMUM_TITLE_LENGTH,
|
||||
global_name='APPEARANCE_MAXIMUM_TITLE_LENGTH', help_text=_(
|
||||
|
||||
@@ -49,10 +49,6 @@
|
||||
url('../fonts/Lato_400italic.ttf') format('truetype');
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-family: "IM Fell English SC", serif;
|
||||
}
|
||||
@@ -71,7 +67,7 @@ body {
|
||||
}
|
||||
|
||||
#carousel-container {
|
||||
overflow-x: scroll; height: 500px;
|
||||
overflow: scroll; height: 100%;
|
||||
}
|
||||
|
||||
#carousel-container img {
|
||||
@@ -142,11 +138,7 @@ hr {
|
||||
}
|
||||
|
||||
.radio ul li {
|
||||
list-style-type:none;
|
||||
}
|
||||
|
||||
a i {
|
||||
padding-right: 3px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.dashboard-widget {
|
||||
@@ -334,3 +326,209 @@ a i {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.pre-server-error {
|
||||
background-color:#ffe7ae;
|
||||
color: black;
|
||||
font-size: 110%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
/*
|
||||
* Top navigation
|
||||
* Hide default border to remove 1px line.
|
||||
*/
|
||||
.navbar-fixed-top {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
/* Hide for mobile, show later */
|
||||
#menu-main {
|
||||
display: none;
|
||||
background-color: #2c3e50;
|
||||
border-right: 1px solid #18bc9c;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-top: 10px;
|
||||
position: fixed;
|
||||
top: 51px;
|
||||
width: 210px;
|
||||
z-index: 1000;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
#menu-main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
text-align: center;
|
||||
width: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Main content
|
||||
*/
|
||||
|
||||
.main {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
margin-left: 210px;
|
||||
}
|
||||
}
|
||||
.main .page-header {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
margin-right: 0px;
|
||||
margin-left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-form > .form-control {
|
||||
padding: 0px;
|
||||
height: 35px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
#accordion-sidebar a {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#accordion-sidebar a[aria-expanded="true"] {
|
||||
background: #1a242f;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar a {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-heading {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-heading:hover {
|
||||
background-color: #517394;
|
||||
}
|
||||
|
||||
#accordion-sidebar > .panel > div > .panel-body > ul > li > a:hover {
|
||||
background-color: #517394;
|
||||
}
|
||||
|
||||
#accordion-sidebar > .panel > div > .panel-body > ul > li.active {
|
||||
background: #1a242f;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-body {
|
||||
font-size: 13px;
|
||||
border: 0px;
|
||||
background-color: #2c3e50;
|
||||
padding-top: 5px;
|
||||
padding-left: 20px;
|
||||
padding-right: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-body li {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#accordion-sidebar .panel-body a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.panel-highlighted {
|
||||
border: 3px solid #18bc9c;
|
||||
}
|
||||
|
||||
.panel-item:hover {
|
||||
box-shadow: 0px 0px 10px #000000;
|
||||
|
||||
}
|
||||
|
||||
.td-container-thumbnail {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
|
||||
/* Side bar */
|
||||
#menu-actions {
|
||||
position: fixed;
|
||||
right: 5px;
|
||||
top: 65px;
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
#viewport {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.has-sidebar {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#sidebar {
|
||||
bottom: 0;
|
||||
display: block;
|
||||
overflow-x: visible;
|
||||
overflow-y: auto;
|
||||
padding-top: 10px;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 100px;
|
||||
width: 150px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.has-sidebar {
|
||||
padding-right: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ var MayanAppClass = MayanApp;
|
||||
|
||||
var partialNavigation = new PartialNavigation({
|
||||
initialURL: initialURL,
|
||||
disabledAnchorClasses: ['disabled'],
|
||||
disabledAnchorClasses: [
|
||||
'btn-multi-item-action', 'disabled', 'pagination-disabled'
|
||||
],
|
||||
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
|
||||
formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess],
|
||||
});
|
||||
|
||||
@@ -1,121 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
class MayanApp {
|
||||
constructor (parameters) {
|
||||
constructor (options) {
|
||||
var self = this;
|
||||
|
||||
parameters = parameters || {}
|
||||
options = options || {
|
||||
ajaxMenusOptions: []
|
||||
}
|
||||
|
||||
this.ajaxSpinnerSeletor = '#ajax-spinner';
|
||||
this.ajaxExecuting = false;
|
||||
this.ajaxMenusOptions = [
|
||||
{
|
||||
app: this,
|
||||
interval: 5000,
|
||||
menuSelector: '#main-menu',
|
||||
url: apiTemplateMainMenuURL,
|
||||
}
|
||||
];
|
||||
this.ajaxMenusOptions = options.ajaxMenusOptions;
|
||||
this.ajaxMenuHashes = {};
|
||||
this.ajaxSpinnerSeletor = '#ajax-spinner';
|
||||
this.window = $(window);
|
||||
}
|
||||
|
||||
// Class methods and variables
|
||||
|
||||
static mayanNotificationBadge (options, data) {
|
||||
// Callback to add the notifications count inside a badge markup
|
||||
var notifications = data[options.attributeName];
|
||||
static countChecked() {
|
||||
var checkCount = $('.check-all-slave:checked').length;
|
||||
|
||||
if (notifications > 0) {
|
||||
// Save the original link text before adding the initial badge markup
|
||||
if (!options.element.data('mn-saved-text')) {
|
||||
options.element.data('mn-saved-text', options.element.html());
|
||||
}
|
||||
|
||||
options.element.html(
|
||||
options.element.data('mn-saved-text') + ' <span class="badge">' + notifications + '</span>'
|
||||
);
|
||||
if (checkCount) {
|
||||
$('#multi-item-title').hide();
|
||||
$('#multi-item-actions').show();
|
||||
} else {
|
||||
if (options.element.data('mn-saved-text')) {
|
||||
// If there is a saved original link text, restore it
|
||||
options.element.html(
|
||||
options.element.data('mn-saved-text')
|
||||
);
|
||||
}
|
||||
$('#multi-item-title').show();
|
||||
$('#multi-item-actions').hide();
|
||||
}
|
||||
}
|
||||
|
||||
static MultiObjectFormProcess ($form, options) {
|
||||
/*
|
||||
* ajaxForm callback to add the external item checkboxes to the
|
||||
* submitted form
|
||||
*/
|
||||
static setupMultiItemActions () {
|
||||
$('body').on('change', '.check-all-slave', function () {
|
||||
MayanApp.countChecked();
|
||||
});
|
||||
|
||||
if ($form.hasClass('form-multi-object-action')) {
|
||||
// Turn form data into an object
|
||||
var formArray = $form.serializeArray().reduce(function (obj, item) {
|
||||
obj[item.name] = item.value;
|
||||
return obj;
|
||||
}, {});
|
||||
$('body').on('click', '.btn-multi-item-action', function (event) {
|
||||
var id_list = [];
|
||||
$('.check-all-slave:checked').each(function (index, value) {
|
||||
//Split the name (ie:"pk_200") and extract only the ID
|
||||
id_list.push(value.name.split('_')[1]);
|
||||
});
|
||||
event.preventDefault();
|
||||
partialNavigation.setLocation(
|
||||
$(this).attr('href') + '?id_list=' + id_list.join(',')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Add all checked checkboxes to the form data
|
||||
$('.form-multi-object-action-checkbox:checked').each(function() {
|
||||
var $this = $(this);
|
||||
formArray[$this.attr('name')] = $this.attr('value');
|
||||
static setupNavBarState () {
|
||||
$('body').on('click', '.a-main-menu-accordion-link', function (event) {
|
||||
console.log('ad');
|
||||
$('.a-main-menu-accordion-link').each(function (index, value) {
|
||||
$(this).parent().removeClass('active');
|
||||
});
|
||||
|
||||
// Set the form data as the data to send
|
||||
options.data = formArray;
|
||||
}
|
||||
$(this).parent().addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
static tagSelectionTemplate (tag, container) {
|
||||
var $tag = $(
|
||||
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
|
||||
);
|
||||
container[0].style.background = tag.element.dataset.color;
|
||||
return $tag;
|
||||
}
|
||||
|
||||
static tagResultTemplate (tag) {
|
||||
if (!tag.element) { return ''; }
|
||||
var $tag = $(
|
||||
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
|
||||
);
|
||||
return $tag;
|
||||
static updateNavbarState () {
|
||||
var uri = new URI(window.location.hash);
|
||||
var uriFragment = uri.fragment();
|
||||
$('.a-main-menu-accordion-link').each(function (index, value) {
|
||||
if (value.pathname === uriFragment) {
|
||||
$(this).closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
|
||||
$(this).parent().addClass('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
|
||||
AJAXperiodicWorker (options) {
|
||||
var app = this;
|
||||
|
||||
$.ajax({
|
||||
complete: function() {
|
||||
if (!options.app) {
|
||||
// Preserve the app reference between consecutive calls
|
||||
options.app = app;
|
||||
}
|
||||
setTimeout(options.app.AJAXperiodicWorker, options.interval, options);
|
||||
},
|
||||
success: function(data) {
|
||||
if (options.callback) {
|
||||
// Conver the callback string to an actual function
|
||||
var callbackFunction = window;
|
||||
|
||||
$.each(options.callback.split('.'), function (index, value) {
|
||||
callbackFunction = callbackFunction[value]
|
||||
});
|
||||
|
||||
callbackFunction(options, data);
|
||||
} else {
|
||||
options.element.text(data[options.attributeName]);
|
||||
}
|
||||
},
|
||||
url: options.APIURL
|
||||
});
|
||||
}
|
||||
|
||||
callbackAJAXSpinnerUpdate () {
|
||||
if (this.ajaxExecuting) {
|
||||
$(this.ajaxSpinnerSeletor).fadeIn(50);
|
||||
@@ -132,10 +87,10 @@ class MayanApp {
|
||||
|
||||
if ((menuHash === undefined) || (menuHash !== data.hex_hash)) {
|
||||
$(options.menuSelector).html(data.html);
|
||||
options.app.ajaxMenuHashes[data.name] = data.hex_hash;
|
||||
if (options.callback !== undefined) {
|
||||
options.callback();
|
||||
}
|
||||
options.app.ajaxMenuHashes[data.name] = data.hex_hash;
|
||||
}
|
||||
},
|
||||
url: options.url,
|
||||
@@ -214,35 +169,24 @@ class MayanApp {
|
||||
}
|
||||
|
||||
initialize () {
|
||||
this.setupAJAXPeriodicWorkers();
|
||||
var self = this;
|
||||
|
||||
this.setupAJAXSpinner();
|
||||
this.setupAutoSubmit();
|
||||
this.setupFormHotkeys();
|
||||
this.setupFullHeightResizing();
|
||||
this.setupItemsSelector();
|
||||
MayanApp.setupMultiItemActions();
|
||||
this.setupNavbarCollapse();
|
||||
MayanApp.setupNavBarState();
|
||||
this.setupNewWindowAnchor();
|
||||
$.each(this.ajaxMenusOptions, function(index, value) {
|
||||
value.app = self;
|
||||
app.doRefreshAJAXMenu(value);
|
||||
});
|
||||
this.setupPanelSelection();
|
||||
partialNavigation.initialize();
|
||||
}
|
||||
|
||||
setupAJAXPeriodicWorkers () {
|
||||
var app = this;
|
||||
|
||||
$('a[data-apw-url]').each(function() {
|
||||
var $this = $(this);
|
||||
|
||||
app.AJAXperiodicWorker({
|
||||
attributeName: $this.data('apw-attribute'),
|
||||
APIURL: $this.data('apw-url'),
|
||||
callback: $this.data('apw-callback'),
|
||||
element: $this,
|
||||
interval: $this.data('apw-interval'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupAJAXSpinner () {
|
||||
var self = this;
|
||||
|
||||
@@ -263,12 +207,19 @@ class MayanApp {
|
||||
});
|
||||
}
|
||||
|
||||
setupAutoSubmit () {
|
||||
$('body').on('change', '.select-auto-submit', function () {
|
||||
if ($(this).val()) {
|
||||
$(this.form).trigger('submit');
|
||||
setupFormHotkeys () {
|
||||
$('body').on('keypress', '.form-hotkey-enter', function (e) {
|
||||
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
|
||||
$(this).find('.btn-hotkey-default').click();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$('body').on('dblclick', '.form-hotkey-double-click', function (e) {
|
||||
$(this).find('.btn-hotkey-default').click();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
setupFullHeightResizing () {
|
||||
@@ -286,9 +237,22 @@ class MayanApp {
|
||||
app.lastChecked = null;
|
||||
|
||||
$('body').on('click', '.check-all', function (event) {
|
||||
var $this = $(this);
|
||||
var checked = $(event.target).prop('checked');
|
||||
var $checkBoxes = $('.check-all-slave');
|
||||
|
||||
if (checked === undefined) {
|
||||
checked = $this.data('checked');
|
||||
checked = !checked;
|
||||
$this.data('checked', checked);
|
||||
|
||||
if (checked) {
|
||||
$this.find('[data-fa-i2svg]').addClass($this.data('icon-checked')).removeClass($this.data('icon-unchecked'));
|
||||
} else {
|
||||
$this.find('[data-fa-i2svg]').addClass($this.data('icon-unchecked')).removeClass($this.data('icon-checked'));
|
||||
}
|
||||
}
|
||||
|
||||
$checkBoxes.prop('checked', checked);
|
||||
$checkBoxes.trigger('change');
|
||||
});
|
||||
@@ -298,6 +262,7 @@ class MayanApp {
|
||||
app.lastChecked = this;
|
||||
return;
|
||||
}
|
||||
|
||||
if(e.shiftKey) {
|
||||
var $checkBoxes = $('.check-all-slave');
|
||||
|
||||
@@ -334,6 +299,58 @@ class MayanApp {
|
||||
});
|
||||
}
|
||||
|
||||
setupPanelSelection () {
|
||||
var app = this;
|
||||
|
||||
// Setup panel highlighting on check
|
||||
$('body').on('change', '.check-all-slave', function (event) {
|
||||
var checked = $(event.target).prop('checked');
|
||||
if (checked) {
|
||||
$(this).closest('.panel-item').addClass('panel-highlighted');
|
||||
} else {
|
||||
$(this).closest('.panel-item').removeClass('panel-highlighted');
|
||||
}
|
||||
});
|
||||
|
||||
$('body').on('click', '.panel-item', function (event) {
|
||||
var $this = $(this);
|
||||
var targetSrc = $(event.target).prop('src');
|
||||
var targetHref = $(event.target).prop('href');
|
||||
var targetIsButton = event.target.tagName === 'BUTTON';
|
||||
var lastChecked = null;
|
||||
|
||||
if ((targetSrc === undefined) && (targetHref === undefined) && (targetIsButton === false)) {
|
||||
var $checkbox = $this.find('.check-all-slave');
|
||||
var checked = $checkbox.prop('checked');
|
||||
|
||||
if (checked) {
|
||||
$checkbox.prop('checked', '');
|
||||
$checkbox.trigger('change');
|
||||
} else {
|
||||
$checkbox.prop('checked', 'checked');
|
||||
$checkbox.trigger('change');
|
||||
}
|
||||
|
||||
if(!app.lastChecked) {
|
||||
app.lastChecked = $checkbox;
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
var $checkBoxes = $('.check-all-slave');
|
||||
|
||||
var start = $checkBoxes.index($checkbox);
|
||||
var end = $checkBoxes.index(app.lastChecked);
|
||||
|
||||
$checkBoxes.slice(
|
||||
Math.min(start, end), Math.max(start, end) + 1
|
||||
).prop('checked', app.lastChecked.prop('checked')).trigger('change');
|
||||
}
|
||||
app.lastChecked = $checkbox;
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupScrollView () {
|
||||
$('.scrollable').scrollview();
|
||||
}
|
||||
@@ -343,12 +360,6 @@ class MayanApp {
|
||||
dropdownAutoWidth: true,
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
$('.select2-tags').select2({
|
||||
templateSelection: MayanApp.tagSelectionTemplate,
|
||||
templateResult: MayanApp.tagResultTemplate,
|
||||
width: '100%'
|
||||
});
|
||||
}
|
||||
|
||||
resizeFullHeight () {
|
||||
|
||||
@@ -6,23 +6,24 @@ class MayanImage {
|
||||
this.load();
|
||||
}
|
||||
|
||||
static intialize () {
|
||||
var app = this;
|
||||
static intialize (options) {
|
||||
this.options = options || {};
|
||||
this.options.templateInvalidDocument = this.options.templateInvalidDocument || '<span>Error loading document image</span>';
|
||||
|
||||
this.fancybox = $().fancybox({
|
||||
animationDuration : 300,
|
||||
buttons : [
|
||||
'fullScreen',
|
||||
'close',
|
||||
],
|
||||
selector: 'a.fancybox',
|
||||
$().fancybox({
|
||||
afterShow: function (instance, current) {
|
||||
$('a.a-caption').on('click', function(event) {
|
||||
instance.close(true);
|
||||
});
|
||||
},
|
||||
animationEffect: 'fade',
|
||||
animationDuration: 100,
|
||||
buttons : [
|
||||
'fullScreen',
|
||||
'close',
|
||||
],
|
||||
infobar: true,
|
||||
|
||||
selector: 'a.fancybox',
|
||||
});
|
||||
|
||||
$('img.lazy-load').lazyload({
|
||||
@@ -42,46 +43,53 @@ class MayanImage {
|
||||
|
||||
$('.lazy-load').one('load', function() {
|
||||
$(this).hide();
|
||||
$(this).fadeIn(300);
|
||||
$(this).show();
|
||||
$(this).siblings('.spinner-container').remove();
|
||||
$(this).removeClass('lazy-load pull-left');
|
||||
clearTimeout(MayanImage.timer);
|
||||
MayanImage.timer = setTimeout(MayanImage.timerFunction, 100);
|
||||
MayanImage.timer = setTimeout(MayanImage.timerFunction, 250);
|
||||
});
|
||||
|
||||
$('.lazy-load-carousel').one('load', function() {
|
||||
$(this).hide();
|
||||
$(this).fadeIn(300);
|
||||
$(this).show();
|
||||
$(this).siblings('.spinner-container').remove();
|
||||
$(this).removeClass('lazy-load-carousel pull-left');
|
||||
});
|
||||
}
|
||||
|
||||
static timerFunction () {
|
||||
$.fn.matchHeight._maintainScroll = true;
|
||||
$.fn.matchHeight._update();
|
||||
}
|
||||
|
||||
load () {
|
||||
var self = this;
|
||||
var container = this.element.parent().parent().parent();
|
||||
var dataURL = this.element.attr('data-url');
|
||||
|
||||
this.element.attr('src', this.element.attr('data-url'));
|
||||
this.element.on('error', function() {
|
||||
// Check the .complete property to see if it is a real error
|
||||
// or it was a cached image
|
||||
if (this.complete === false) {
|
||||
// It is a cached image, set the src attribute to trigger
|
||||
// it's display.
|
||||
this.src = this.src;
|
||||
} else {
|
||||
container.html(MayanImage.templateInvalidDocument);
|
||||
}
|
||||
});
|
||||
|
||||
$.fn.matchHeight._maintainScroll = true;
|
||||
if (dataURL === '') {
|
||||
container.html(MayanImage.options.templateInvalidDocument);
|
||||
} else {
|
||||
this.element.attr('src', dataURL);
|
||||
setTimeout(function () {
|
||||
self.element.on('error', function () {
|
||||
// Check the .complete property to see if it is a real
|
||||
// error or it was a cached image
|
||||
if (this.complete === false) {
|
||||
// It is a cached image, set the src attribute to
|
||||
// trigger it's display.
|
||||
this.src = dataURL;
|
||||
} else {
|
||||
container.html(
|
||||
MayanImage.options.templateInvalidDocument
|
||||
);
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MayanImage.templateInvalidDocument = $('#template-invalid-document').html();
|
||||
MayanImage.timer = setTimeout(null);
|
||||
|
||||
$.fn.matchHeight._maintainScroll = true;
|
||||
|
||||
@@ -160,7 +160,21 @@ class PartialNavigation {
|
||||
*/
|
||||
|
||||
if (djangoDEBUG) {
|
||||
$('#ajax-content').html('<pre class="text-primary" style="background-color:#ffe7ae"><code>' + jqXHR.statusText + '</code></pre>');
|
||||
var errorMessage = null;
|
||||
|
||||
if (jqXHR.status != 0) {
|
||||
errorMessage = jqXHR.responseText || jqXHR.statusText;
|
||||
} else {
|
||||
errorMessage = 'Server communication error.';
|
||||
}
|
||||
|
||||
$('#ajax-content').html(
|
||||
' \
|
||||
<div class="alert alert-danger" role="alert"><i class="fa fa-exclamation-triangle"></i> Server Error, status code: ' + jqXHR.status + '</div> \
|
||||
<pre class="pre-server-error"><code>' + errorMessage +'</code> \
|
||||
</pre> \
|
||||
'
|
||||
);
|
||||
} else {
|
||||
if (jqXHR.status == 0) {
|
||||
$('#modal-server-error .modal-body').html($('#template-error').html());
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.6.3.tgz",
|
||||
"integrity": "sha512-s5PLdI9NYgjBvfrv6rhirPHlAHWx+Sfo/IjsAeiXYfmemC/GSjwsyz1wLnGPazbLPXWfk62ks980o9AmsxYUEQ=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "=5.6.3",
|
||||
"bootstrap": "=3.3.7",
|
||||
"bootswatch": "=3.3.7",
|
||||
"jquery": "=3.3.1",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
Font Awesome Free License
|
||||
-------------------------
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license.
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
In the Font Awesome Free download, the CC BY 4.0 license applies to all icons
|
||||
packaged as SVG and JS file types.
|
||||
|
||||
# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL)
|
||||
In the Font Awesome Free download, the SIL OLF license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
# Attribution
|
||||
Attribution is required by MIT, SIL OLF, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
# Brand Icons
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
||||
@@ -1,7 +0,0 @@
|
||||
# Font Awesome 5.0.8
|
||||
|
||||
Thanks for downloading Font Awesome! We're so excited you're here.
|
||||
|
||||
Our documentation is available online. Just head here:
|
||||
|
||||
https://fontawesome.com
|
||||
@@ -1,289 +0,0 @@
|
||||
area-chart:
|
||||
name: chart-area
|
||||
arrow-circle-o-down:
|
||||
name: arrow-alt-circle-down
|
||||
prefix: far
|
||||
arrow-circle-o-left:
|
||||
name: arrow-alt-circle-left
|
||||
prefix: far
|
||||
arrow-circle-o-right:
|
||||
name: arrow-alt-circle-right
|
||||
prefix: far
|
||||
arrow-circle-o-up:
|
||||
name: arrow-alt-circle-up
|
||||
prefix: far
|
||||
arrows:
|
||||
name: arrows-alt
|
||||
arrows-alt:
|
||||
name: expand-arrows-alt
|
||||
arrows-h:
|
||||
name: arrows-alt-h
|
||||
arrows-v:
|
||||
name: arrows-alt-v
|
||||
bar-chart:
|
||||
name: chart-bar
|
||||
prefix: far
|
||||
bitbucket-square:
|
||||
name: bitbucket
|
||||
prefix: fab
|
||||
calendar:
|
||||
name: calendar-alt
|
||||
calendar-o:
|
||||
name: calendar
|
||||
prefix: far
|
||||
caret-square-o-down:
|
||||
name: caret-square-down
|
||||
prefix: far
|
||||
caret-square-o-left:
|
||||
name: caret-square-left
|
||||
prefix: far
|
||||
caret-square-o-right:
|
||||
name: caret-square-right
|
||||
prefix: far
|
||||
caret-square-o-up:
|
||||
name: caret-square-up
|
||||
prefix: far
|
||||
cc:
|
||||
name: closed-captioning
|
||||
prefix: far
|
||||
chain-broken:
|
||||
name: unlink
|
||||
circle-o-notch:
|
||||
name: circle-notch
|
||||
circle-thin:
|
||||
name: circle
|
||||
prefix: far
|
||||
clipboard:
|
||||
prefix: far
|
||||
clone:
|
||||
prefix: far
|
||||
cloud-download:
|
||||
name: cloud-download-alt
|
||||
cloud-upload:
|
||||
name: cloud-upload-alt
|
||||
code-fork:
|
||||
name: code-branch
|
||||
commenting:
|
||||
name: comment-alt
|
||||
compass:
|
||||
prefix: far
|
||||
copyright:
|
||||
prefix: far
|
||||
creative-commons:
|
||||
prefix: fab
|
||||
credit-card:
|
||||
prefix: far
|
||||
credit-card-alt:
|
||||
name: credit-card
|
||||
cutlery:
|
||||
name: utensils
|
||||
diamond:
|
||||
name: gem
|
||||
prefix: far
|
||||
eercast:
|
||||
name: sellcast
|
||||
prefix: fab
|
||||
eur:
|
||||
name: euro-sign
|
||||
exchange:
|
||||
name: exchange-alt
|
||||
external-link:
|
||||
name: external-link-alt
|
||||
external-link-square:
|
||||
name: external-link-square-alt
|
||||
eye-dropper:
|
||||
name: eye-dropper
|
||||
prefix: far
|
||||
eye-slash:
|
||||
prefix: far
|
||||
eyedropper:
|
||||
name: eye-dropper
|
||||
facebook:
|
||||
name: facebook-f
|
||||
prefix: fab
|
||||
facebook-official:
|
||||
name: facebook
|
||||
prefix: fab
|
||||
file-text:
|
||||
name: file-alt
|
||||
files-o:
|
||||
name: copy
|
||||
prefix: far
|
||||
floppy-o:
|
||||
name: save
|
||||
prefix: far
|
||||
gbp:
|
||||
name: pound-sign
|
||||
glass:
|
||||
name: glass-martini
|
||||
google-plus:
|
||||
name: google-plus-g
|
||||
prefix: fab
|
||||
google-plus-circle:
|
||||
name: google-plus
|
||||
prefix: fab
|
||||
google-plus-official:
|
||||
name: google-plus
|
||||
prefix: fab
|
||||
hand-o-down:
|
||||
name: hand-point-down
|
||||
prefix: far
|
||||
hand-o-left:
|
||||
name: hand-point-left
|
||||
prefix: far
|
||||
hand-o-right:
|
||||
name: hand-point-right
|
||||
prefix: far
|
||||
hand-o-up:
|
||||
name: hand-point-up
|
||||
prefix: far
|
||||
header:
|
||||
name: heading
|
||||
id-badge:
|
||||
prefix: far
|
||||
ils:
|
||||
name: shekel-sign
|
||||
inr:
|
||||
name: rupee-sign
|
||||
intersex:
|
||||
name: transgender
|
||||
jpy:
|
||||
name: yen-sign
|
||||
krw:
|
||||
name: won-sign
|
||||
level-down:
|
||||
name: level-down-alt
|
||||
level-up:
|
||||
name: level-up-alt
|
||||
life-ring:
|
||||
prefix: far
|
||||
line-chart:
|
||||
name: chart-line
|
||||
linkedin:
|
||||
name: linkedin-in
|
||||
prefix: fab
|
||||
linkedin-square:
|
||||
name: linkedin
|
||||
prefix: fab
|
||||
list-alt:
|
||||
prefix: far
|
||||
long-arrow-down:
|
||||
name: long-arrow-alt-down
|
||||
long-arrow-left:
|
||||
name: long-arrow-alt-left
|
||||
long-arrow-right:
|
||||
name: long-arrow-alt-right
|
||||
long-arrow-up:
|
||||
name: long-arrow-alt-up
|
||||
map-marker:
|
||||
name: map-marker-alt
|
||||
meanpath:
|
||||
name: font-awesome
|
||||
prefix: fab
|
||||
mobile:
|
||||
name: mobile-alt
|
||||
money:
|
||||
name: money-bill-alt
|
||||
prefix: far
|
||||
object-group:
|
||||
prefix: far
|
||||
object-ungroup:
|
||||
prefix: far
|
||||
paste:
|
||||
prefix: far
|
||||
pencil:
|
||||
name: pencil-alt
|
||||
pencil-square:
|
||||
name: pen-square
|
||||
pencil-square-o:
|
||||
name: edit
|
||||
prefix: far
|
||||
picture:
|
||||
name: image
|
||||
pie-chart:
|
||||
name: chart-pie
|
||||
refresh:
|
||||
name: sync
|
||||
registered:
|
||||
prefix: far
|
||||
repeat:
|
||||
name: redo
|
||||
rub:
|
||||
name: ruble-sign
|
||||
scissors:
|
||||
name: cut
|
||||
shield:
|
||||
name: shield-alt
|
||||
sign-in:
|
||||
name: sign-in-alt
|
||||
sign-out:
|
||||
name: sign-out-alt
|
||||
sliders:
|
||||
name: sliders-h
|
||||
sort-alpha-asc:
|
||||
name: sort-alpha-down
|
||||
sort-alpha-desc:
|
||||
name: sort-alpha-up
|
||||
sort-amount-asc:
|
||||
name: sort-amount-down
|
||||
sort-amount-desc:
|
||||
name: sort-amount-up
|
||||
sort-asc:
|
||||
name: sort-up
|
||||
sort-desc:
|
||||
name: sort-down
|
||||
sort-numeric-asc:
|
||||
name: sort-numeric-down
|
||||
sort-numeric-desc:
|
||||
name: sort-numeric-up
|
||||
spoon:
|
||||
name: utensil-spoon
|
||||
star-half-empty:
|
||||
name: star-half
|
||||
star-half-full:
|
||||
name: star-half
|
||||
support:
|
||||
name: life-ring
|
||||
prefix: far
|
||||
tablet:
|
||||
name: tablet-alt
|
||||
tachometer:
|
||||
name: tachometer-alt
|
||||
television:
|
||||
name: tv
|
||||
thumb-tack:
|
||||
name: thumbtack
|
||||
thumbs-o-down:
|
||||
name: thumbs-down
|
||||
prefix: far
|
||||
thumbs-o-up:
|
||||
name: thumbs-up
|
||||
prefix: far
|
||||
ticket:
|
||||
name: ticket-alt
|
||||
trash:
|
||||
name: trash-alt
|
||||
trash-o:
|
||||
name: trash-alt
|
||||
prefix: far
|
||||
try:
|
||||
name: lira-sign
|
||||
usd:
|
||||
name: dollar-sign
|
||||
video-camera:
|
||||
name: video
|
||||
vimeo:
|
||||
name: vimeo-v
|
||||
prefix: fab
|
||||
volume-control-phone:
|
||||
name: phone-volume
|
||||
wheelchair-alt:
|
||||
name: accessible-icon
|
||||
prefix: fab
|
||||
window-maximize:
|
||||
prefix: far
|
||||
window-restore:
|
||||
prefix: far
|
||||
youtube-play:
|
||||
name: youtube
|
||||
prefix: fab
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 775 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M482.2 372.1C476.5 365.2 250 75 242.3 65.5c-13.7-17.2 0-16.8 19.2-16.9 9.7-.1 106.3-.6 116.5-.6 24.1-.1 28.7.6 38.4 12.8 2.1 2.7 205.1 245.8 207.2 248.3 5.5 6.7 15.2 19.1 7.2 23.4-2.4 1.3-114.6 47.7-117.8 48.9-10.1 4-17.5 6.8-30.8-9.3m114.7-5.6s-115 50.4-117.5 51.6c-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1.2-9.6.8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6.2 2.5 2 2.6 4.6 3.5 2.7.8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2-2.3-.3-24.6-4.7-38-7.2m-326-181.3s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7.8 0 114.5-36.6 114.5-36.6.5-.6-.1-.1.6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3v-1l-51.2-65.8" class="st1"/></svg>
|
||||
|
Before Width: | Height: | Size: 858 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M248 167.5l64.9 98.8H183.1l64.9-98.8zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-99.8 82.7L248 115.5 99.8 338.7h30.4l33.6-51.7h168.6l33.6 51.7h30.2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 267 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M482.1 32H28.7C5.8 32 0 37.9 0 60.9v390.2C0 474.4 5.8 480 28.7 480h453.4c24.4 0 29.9-5.2 29.9-29.7V62.2c0-24.6-5.4-30.2-29.9-30.2zM178.4 220.3c-27.5-20.2-72.1-8.7-84.2 23.4-4.3 11.1-9.3 9.5-17.5 8.3-9.7-1.5-17.2-3.2-22.5-5.5-28.8-11.4 8.6-55.3 24.9-64.3 41.1-21.4 83.4-22.2 125.3-4.8 40.9 16.8 34.5 59.2 34.5 128.5 2.7 25.8-4.3 58.3 9.3 88.8 1.9 4.4.4 7.9-2.7 10.7-8.4 6.7-39.3 2.2-46.6-7.4-1.9-2.2-1.8-3.6-3.9-6.2-3.6-3.9-7.3-2.2-11.9 1-57.4 36.4-140.3 21.4-147-43.3-3.1-29.3 12.4-57.1 39.6-71 38.2-19.5 112.2-11.8 114-30.9 1.1-10.2-1.9-20.1-11.3-27.3zm286.7 222c0 15.1-11.1 9.9-17.8 9.9H52.4c-7.4 0-18.2 4.8-17.8-10.7.4-13.9 10.5-9.1 17.1-9.1 132.3-.4 264.5-.4 396.8 0 6.8 0 16.6-4.4 16.6 9.9zm3.8-340.5v291c0 5.7-.7 13.9-8.1 13.9-12.4-.4-27.5 7.1-36.1-5.6-5.8-8.7-7.8-4-12.4-1.2-53.4 29.7-128.1 7.1-144.4-85.2-6.1-33.4-.7-67.1 15.7-100 11.8-23.9 56.9-76.1 136.1-30.5v-71c0-26.2-.1-26.2 26-26.2 3.1 0 6.6.4 9.7 0 10.1-.8 13.6 4.4 13.6 14.3-.1.2-.1.3-.1.5zm-51.5 232.3c-19.5 47.6-72.9 43.3-90 5.2-15.1-33.3-15.5-68.2.4-101.5 16.3-34.1 59.7-35.7 81.5-4.8 20.6 28.8 14.9 84.6 8.1 101.1zm-294.8 35.3c-7.5-1.3-33-3.3-33.7-27.8-.4-13.9 7.8-23 19.8-25.8 24.4-5.9 49.3-9.9 73.7-14.7 8.9-2 7.4 4.4 7.8 9.5 1.4 33-26.1 59.2-67.6 58.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M159.7 237.4C108.4 308.3 43.1 348.2 14 326.6-15.2 304.9 2.8 230 54.2 159.1c51.3-70.9 116.6-110.8 145.7-89.2 29.1 21.6 11.1 96.6-40.2 167.5zm351.2-57.3C437.1 303.5 319 367.8 246.4 323.7c-25-15.2-41.3-41.2-49-73.8-33.6 64.8-92.8 113.8-164.1 133.2 49.8 59.3 124.1 96.9 207 96.9 150 0 271.6-123.1 271.6-274.9.1-8.5-.3-16.8-1-25z"/></svg>
|
||||
|
Before Width: | Height: | Size: 404 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M229.3 182.6c-49.3 0-89.2 39.9-89.2 89.2 0 49.3 39.9 89.2 89.2 89.2s89.2-39.9 89.2-89.2c0-49.3-40-89.2-89.2-89.2zm62.7 56.6l-58.9 30.6c-1.8.9-3.8-.4-3.8-2.3V201c0-1.5 1.3-2.7 2.7-2.6 26.2 1 48.9 15.7 61.1 37.1.7 1.3.2 3-1.1 3.7zM389.1 32H58.9C26.4 32 0 58.4 0 90.9V421c0 32.6 26.4 59 58.9 59H389c32.6 0 58.9-26.4 58.9-58.9V90.9C448 58.4 421.6 32 389.1 32zm-202.6 84.7c0-10.8 8.7-19.5 19.5-19.5h45.3c10.8 0 19.5 8.7 19.5 19.5v15.4c0 1.8-1.7 3-3.3 2.5-12.3-3.4-25.1-5.1-38.1-5.1-13.5 0-26.7 1.8-39.4 5.5-1.7.5-3.4-.8-3.4-2.5v-15.8zm-84.4 37l9.2-9.2c7.6-7.6 19.9-7.6 27.5 0l7.7 7.7c1.1 1.1 1 3-.3 4-6.2 4.5-12.1 9.4-17.6 14.9-5.4 5.4-10.4 11.3-14.8 17.4-1 1.3-2.9 1.5-4 .3l-7.7-7.7c-7.6-7.5-7.6-19.8 0-27.4zm127.2 244.8c-70 0-126.6-56.7-126.6-126.6s56.7-126.6 126.6-126.6c70 0 126.6 56.6 126.6 126.6 0 69.8-56.7 126.6-126.6 126.6z"/></svg>
|
||||
|
Before Width: | Height: | Size: 907 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 611.2 512"><path d="M0 325.2c2.3-4.2 5.2-4.9 9.7-2.5 10.4 5.6 20.6 11.4 31.2 16.7 40.7 20.4 83.2 35.6 127.4 46.3 20.9 5 41.9 9 63.2 11.8 31.5 4.2 63.2 6 95 5.2 17.4-.4 34.8-1.8 52.1-3.8 56.4-6.7 110.9-20.8 163.3-42.8 2.9-1.2 5.9-2 9.1-1.2 6.7 1.8 9 9 4.1 13.9-2.8 2.8-6.3 5.1-9.6 7.4-30.7 21.1-64.2 36.4-99.6 47.9-24.6 7.9-49.6 13.8-75.1 17.6-17.6 2.6-35.4 4.4-53.2 4.8-.8 0-1.7.2-2.5.3H294c-.8-.1-1.7-.3-2.5-.3-3.6-.2-7.2-.3-10.7-.4-16.9-.7-33.7-2.6-50.4-5.3-27.4-4.5-54.2-11.4-80.4-20.9-54.1-19.6-102.6-48.6-145.6-87-1.8-1.6-3-3.8-4.4-5.7v-2zM158 65c-1.4.2-2.9.4-4.3.6-14 1.7-26.6 6.9-38 15.1-2.4 1.7-4.6 3.5-7.1 5.4-.2-.5-.4-1-.4-1.4-.4-2.7-.8-5.5-1.3-8.2-.7-4.6-3-6.6-7.6-6.6H87.8c-6.9 0-8.2 1.3-8.2 8.2v209.3c0 1 0 2 .1 3 .2 3 2 4.9 4.9 5 7 .1 14.1.1 21.1 0 2.9 0 4.7-2 5-5 .1-1 .1-2 .1-3V215c1.1.9 1.7 1.4 2.2 1.9 17.9 14.9 38.5 19.8 61 15.4 20.4-4 34.6-16.5 43.8-34.9 7-13.9 9.9-28.7 10.3-44.1.5-17.1-1.2-33.9-8.1-49.8-8.5-19.6-22.6-32.5-43.9-36.9-3.2-.7-6.5-1-9.8-1.5-2.8-.1-5.5-.1-8.3-.1zm-47.4 41.9c0-1.5.4-2.4 1.7-3.3 13.7-9.5 28.8-14.5 45.6-13.2 14.9 1.1 27.1 8.4 33.5 25.9 3.9 10.7 4.9 21.8 4.9 33 0 10.4-.8 20.6-4 30.6-6.8 21.3-22.4 29.4-42.6 28.5-14-.6-26.2-6-37.4-13.9-1.2-.9-1.7-1.7-1.7-3.3.1-14.1 0-28.1 0-42.2 0-14 .1-28 0-42.1zM316.3 65c-1 .1-2 .3-2.9.4-9.8.5-19.4 1.7-28.9 4.1-6.1 1.6-12 3.8-17.9 5.8-3.6 1.2-5.4 3.8-5.3 7.7.1 3.3-.1 6.6 0 9.9.1 4.8 2.1 6.1 6.8 4.9 7.8-2 15.6-4.2 23.5-5.7 12.3-2.3 24.7-3.3 37.2-1.4 6.5 1 12.6 2.9 16.8 8.4 3.7 4.8 5.1 10.5 5.3 16.4.3 8.3.2 16.6.3 24.9 0 .4-.1.9-.2 1.4-.5-.1-.9 0-1.3-.1-10.5-2.5-21.1-4.3-32-4.9-11.3-.6-22.5.1-33.3 3.9-12.9 4.5-23.3 12.3-29.4 24.9-4.7 9.8-5.4 20.2-3.9 30.7 2 14 9 24.8 21.4 31.7 11.9 6.6 24.8 7.4 37.9 5.4 15.1-2.3 28.5-8.7 40.3-18.4.4-.4.9-.7 1.6-1.1.6 3.8 1.1 7.4 1.8 11 .6 3.1 2.5 5.1 5.4 5.2 5.4.1 10.9.1 16.3 0 2.7-.1 4.5-1.9 4.8-4.7.1-.9.1-1.9.1-2.8v-106c0-4.3-.2-8.6-.9-12.9-1.9-12.9-7.4-23.5-19-30.4-6.7-4-14.1-6-21.8-7.1-3.6-.5-7.2-.8-10.8-1.3-3.9.1-7.9.1-11.9.1zm35 127.7c0 1.3-.4 2.2-1.5 3-11.2 8.1-23.5 13.5-37.4 14.9-5.7.6-11.4.4-16.8-1.8-6.3-2.5-10.4-6.9-12.4-13.3s-2-13-.1-19.4c2.5-8.3 8.4-13 16.4-15.6 8.1-2.6 16.5-3 24.8-2.2 8.4.7 16.6 2.3 25 3.4 1.6.2 2.1 1 2.1 2.6-.1 4.8 0 9.5 0 14.3-.1 4.7-.2 9.4-.1 14.1zm259.9 129.4c-1-5-4.8-6.9-9.1-8.3-6.8-2.3-13.9-3.3-21-3.9-13.1-1.1-26.2-.5-39.2 1.9-14.3 2.7-27.9 7.3-40 15.6-1.4 1-2.8 2.1-3.7 3.5-.7 1.1-.9 2.8-.5 4 .4 1.5 2.1 1.9 3.6 1.8.7 0 1.5 0 2.2-.1 7.8-.8 15.5-1.7 23.3-2.5 11.4-1.1 22.9-1.8 34.3-.9 4.8.3 9.7 1.4 14.4 2.7 5.1 1.4 7.4 5.2 7.6 10.4.4 8-1.4 15.7-3.5 23.3-4.1 15.4-10 30.3-15.8 45.1-.4 1-.8 2-1 3-.5 2.9 1.2 4.8 4.1 4.1 1.7-.4 3.6-1.3 4.8-2.5 4.4-4.3 8.9-8.6 12.7-13.4 12.8-16.4 20.3-35.3 24.7-55.6.8-3.6 1.4-7.3 2.1-10.9v-17.3zM479.1 198.9c-12.9-35.7-25.8-71.5-38.7-107.2-2-5.7-4.2-11.3-6.3-16.9-1.1-2.9-3.2-4.8-6.4-4.8-7.6-.1-15.2-.2-22.9-.1-2.5 0-3.7 2-3.2 4.5.5 2.1 1.1 4.1 1.9 6.1 19.6 48.5 39.3 97.1 59.1 145.5 1.7 4.1 2.1 7.6.2 11.8-3.3 7.3-5.9 15-9.3 22.3-3 6.5-8 11.4-15.2 13.3-5.1 1.4-10.2 1.6-15.4 1.1-2.5-.2-5-.8-7.5-1-3.4-.2-5.1 1.3-5.2 4.8-.1 3.3-.1 6.6 0 9.9.1 5.5 2 8 7.4 8.9 5.6 1 11.3 1.9 16.9 2 17.1.4 30.7-6.5 39.5-21.4 3.5-5.9 6.7-12.1 9.2-18.4 23.7-59.8 47.1-119.7 70.6-179.6.7-1.8 1.3-3.6 1.6-5.5.4-2.8-.9-4.4-3.7-4.4-6.6-.1-13.3 0-19.9 0-3.7 0-6.3 1.6-7.7 5.2-.5 1.4-1.1 2.7-1.6 4.1-11.6 33.3-23.2 66.6-34.8 100-2.5 7.2-5.1 14.5-7.7 22.2-.4-1.1-.6-1.7-.9-2.4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M257.2 162.7c-48.7 1.8-169.5 15.5-169.5 117.5 0 109.5 138.3 114 183.5 43.2 6.5 10.2 35.4 37.5 45.3 46.8l56.8-56S341 288.9 341 261.4V114.3C341 89 316.5 32 228.7 32 140.7 32 94 87 94 136.3l73.5 6.8c16.3-49.5 54.2-49.5 54.2-49.5 40.7-.1 35.5 29.8 35.5 69.1zm0 86.8c0 80-84.2 68-84.2 17.2 0-47.2 50.5-56.7 84.2-57.8v40.6zm136 163.5c-7.7 10-70 67-174.5 67S34.2 408.5 9.7 379c-6.8-7.7 1-11.3 5.5-8.3C88.5 415.2 203 488.5 387.7 401c7.5-3.7 13.3 2 5.5 12zm39.8 2.2c-6.5 15.8-16 26.8-21.2 31-5.5 4.5-9.5 2.7-6.5-3.8s19.3-46.5 12.7-55c-6.5-8.3-37-4.3-48-3.2-10.8 1-13 2-14-.3-2.3-5.7 21.7-15.5 37.5-17.5 15.7-1.8 41-.8 46 5.7 3.7 5.1 0 27.1-6.5 43.1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 720 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M240.1 32c-61.9 0-131.5 16.9-184.2 55.4-5.1 3.1-9.1 9.2-7.2 19.4 1.1 5.1 5.1 27.4 10.2 39.6 4.1 10.2 14.2 10.2 20.3 6.1 32.5-22.3 96.5-47.7 152.3-47.7 57.9 0 58.9 28.4 58.9 73.1v38.5C203 227.7 78.2 251 46.7 264.2 11.2 280.5 16.3 357.7 16.3 376s15.2 104 124.9 104c47.8 0 113.7-20.7 153.3-42.1v25.4c0 3 2.1 8.2 6.1 9.1 3.1 1 50.7 2 59.9 2s62.5.3 66.5-.7c4.1-1 5.1-6.1 5.1-9.1V168c-.1-80.3-57.9-136-192-136zm-87.9 327.7c0-12.2-3-42.7 18.3-52.9 24.3-13.2 75.1-29.4 119.8-33.5V380c-21.4 13.2-48.7 24.4-79.1 24.4-52.8 0-58.9-33.5-59-44.7"/></svg>
|
||||
|
Before Width: | Height: | Size: 611 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M89.6 204.5v115.8c0 15.4-12.1 27.7-27.5 27.7-15.3 0-30.1-12.4-30.1-27.7V204.5c0-15.1 14.8-27.5 30.1-27.5 15.1 0 27.5 12.4 27.5 27.5zm10.8 157c0 16.4 13.2 29.6 29.6 29.6h19.9l.3 61.1c0 36.9 55.2 36.6 55.2 0v-61.1h37.2v61.1c0 36.7 55.5 36.8 55.5 0v-61.1h20.2c16.2 0 29.4-13.2 29.4-29.6V182.1H100.4v179.4zm248-189.1H99.3c0-42.8 25.6-80 63.6-99.4l-19.1-35.3c-2.8-4.9 4.3-8 6.7-3.8l19.4 35.6c34.9-15.5 75-14.7 108.3 0L297.5 34c2.5-4.3 9.5-1.1 6.7 3.8L285.1 73c37.7 19.4 63.3 56.6 63.3 99.4zm-170.7-55.5c0-5.7-4.6-10.5-10.5-10.5-5.7 0-10.2 4.8-10.2 10.5s4.6 10.5 10.2 10.5c5.9 0 10.5-4.8 10.5-10.5zm113.4 0c0-5.7-4.6-10.5-10.2-10.5-5.9 0-10.5 4.8-10.5 10.5s4.6 10.5 10.5 10.5c5.6 0 10.2-4.8 10.2-10.5zm94.8 60.1c-15.1 0-27.5 12.1-27.5 27.5v115.8c0 15.4 12.4 27.7 27.5 27.7 15.4 0 30.1-12.4 30.1-27.7V204.5c0-15.4-14.8-27.5-30.1-27.5z"/></svg>
|
||||
|
Before Width: | Height: | Size: 907 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M347.1 215.4c11.7-32.6 45.4-126.9 45.4-157.1 0-26.6-15.7-48.9-43.7-48.9-44.6 0-84.6 131.7-97.1 163.1C242 144 196.6 0 156.6 0c-31.1 0-45.7 22.9-45.7 51.7 0 35.3 34.2 126.8 46.6 162-6.3-2.3-13.1-4.3-20-4.3-23.4 0-48.3 29.1-48.3 52.6 0 8.9 4.9 21.4 8 29.7-36.9 10-51.1 34.6-51.1 71.7C46 435.6 114.4 512 210.6 512c118 0 191.4-88.6 191.4-202.9 0-43.1-6.9-82-54.9-93.7zM311.7 108c4-12.3 21.1-64.3 37.1-64.3 8.6 0 10.9 8.9 10.9 16 0 19.1-38.6 124.6-47.1 148l-34-6 33.1-93.7zM142.3 48.3c0-11.9 14.5-45.7 46.3 47.1l34.6 100.3c-15.6-1.3-27.7-3-35.4 1.4-10.9-28.8-45.5-119.7-45.5-148.8zM140 244c29.3 0 67.1 94.6 67.1 107.4 0 5.1-4.9 11.4-10.6 11.4-20.9 0-76.9-76.9-76.9-97.7.1-7.7 12.7-21.1 20.4-21.1zm184.3 186.3c-29.1 32-66.3 48.6-109.7 48.6-59.4 0-106.3-32.6-128.9-88.3-17.1-43.4 3.8-68.3 20.6-68.3 11.4 0 54.3 60.3 54.3 73.1 0 4.9-7.7 8.3-11.7 8.3-16.1 0-22.4-15.5-51.1-51.4-29.7 29.7 20.5 86.9 58.3 86.9 26.1 0 43.1-24.2 38-42 3.7 0 8.3.3 11.7-.6 1.1 27.1 9.1 59.4 41.7 61.7 0-.9 2-7.1 2-7.4 0-17.4-10.6-32.6-10.6-50.3 0-28.3 21.7-55.7 43.7-71.7 8-6 17.7-9.7 27.1-13.1 9.7-3.7 20-8 27.4-15.4-1.1-11.2-5.7-21.1-16.9-21.1-27.7 0-120.6 4-120.6-39.7 0-6.7.1-13.1 17.4-13.1 32.3 0 114.3 8 138.3 29.1 18.1 16.1 24.3 113.2-31 174.7zm-98.6-126c9.7 3.1 19.7 4 29.7 6-7.4 5.4-14 12-20.3 19.1-2.8-8.5-6.2-16.8-9.4-25.1z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M640 238.2l-3.2 28.2-34.5 2.3-2 18.1 34.5-2.3-3.2 28.2-34.4 2.2-2.3 20.1 34.4-2.2-3 26.1-64.7 4.1 12.7-113.2L527 365.2l-31.9 2-23.8-117.8 30.3-2 13.6 79.4 31.7-82.4 93.1-6.2zM426.8 371.5l28.3-1.8L468 249.6l-28.4 1.9-12.8 120zM162 388.1l-19.4-36-3.5 37.4-28.2 1.7 2.7-29.1c-11 18-32 34.3-56.9 35.8C23.9 399.9-3 377 .3 339.7c2.6-29.3 26.7-62.8 67.5-65.4 37.7-2.4 47.6 23.2 51.3 28.8l2.8-30.8 38.9-2.5c20.1-1.3 38.7 3.7 42.5 23.7l2.6-26.6 64.8-4.2-2.7 27.9-36.4 2.4-1.7 17.9 36.4-2.3-2.7 27.9-36.4 2.3-1.9 19.9 36.3-2.3-2.1 20.8 55-117.2 23.8-1.6L370.4 369l8.9-85.6-22.3 1.4 2.9-27.9 75-4.9-3 28-24.3 1.6-9.7 91.9-58 3.7-4.3-15.6-39.4 2.5-8 16.3-126.2 7.7zm-44.3-70.2l-26.4 1.7C84.6 307.2 76.9 303 65 303.8c-19 1.2-33.3 17.5-34.6 33.3-1.4 16 7.3 32.5 28.7 31.2 12.8-.8 21.3-8.6 28.9-18.9l27-1.7 2.7-29.8zm56.1-7.7c1.2-12.9-7.6-13.6-26.1-12.4l-2.7 28.5c14.2-.9 27.5-2.1 28.8-16.1zm21.1 70.8l5.8-60c-5 13.5-14.7 21.1-27.9 26.6l22.1 33.4zm135.4-45l-7.9-37.8-15.8 39.3 23.7-1.5zm-170.1-74.6l-4.3-17.5-39.6 2.6-8.1 18.2-31.9 2.1 57-121.9 23.9-1.6 30.7 102 9.9-104.7 27-1.8 37.8 63.6 6.5-66.6 28.5-1.9-4 41.2c7.4-13.5 22.9-44.7 63.6-47.5 40.5-2.8 52.4 29.3 53.4 30.3l3.3-32 39.3-2.7c12.7-.9 27.8.3 36.3 9.7l-4.4-11.9 32.2-2.2 12.9 43.2 23-45.7 31-2.2-43.6 78.4-4.8 44.3-28.4 1.9 4.8-44.3-15.8-43c1 22.3-9.2 40.1-32 49.6l25.2 38.8-36.4 2.4-19.2-36.8-4 38.3-28.4 1.9 3.3-31.5c-6.7 9.3-19.7 35.4-59.6 38-26.2 1.7-45.6-10.3-55.4-39.2l-4 40.3-25 1.6-37.6-63.3-6.3 66.2-56.8 3.7zm276.6-82.1c10.2-.7 17.5-2.1 21.6-4.3 4.5-2.4 7-6.4 7.6-12.1.6-5.3-.6-8.8-3.4-10.4-3.6-2.1-10.6-2.8-22.9-2l-2.9 28.8zM327.7 214c5.6 5.9 12.7 8.5 21.3 7.9 4.7-.3 9.1-1.8 13.3-4.1 5.5-3 10.6-8 15.1-14.3l-34.2 2.3 2.4-23.9 63.1-4.3 1.2-12-31.2 2.1c-4.1-3.7-7.8-6.6-11.1-8.1-4-1.7-8.1-2.8-12.2-2.5-8 .5-15.3 3.6-22 9.2-7.7 6.4-12 14.5-12.9 24.4-1.1 9.6 1.4 17.3 7.2 23.3zm-201.3 8.2l23.8-1.6-8.3-37.6-15.5 39.2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 415.6 512"><path d="M169.7 268.1h76.2l-38.1-91.6-38.1 91.6zM207.8 32L0 106.4l31.8 275.7 176 97.9 176-97.9 31.8-275.7L207.8 32zM338 373.8h-48.6l-26.2-65.4H152.6l-26.2 65.4H77.7L207.8 81.5 338 373.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 259 B |