Compare commits

...

469 Commits

Author SHA1 Message Date
Roberto Rosario
c072a24890 Merge branch 'use-preparestatic' into 'versions/next'
Switch to preparestatic (Closes: #593)

See merge request mayan-edms/mayan-edms!48
2019-05-25 00:11:09 +00:00
Jakob Haufe
fcfb705fb3 Switch to preparestatic (Closes: #593)
collectstatic failes on various test suite files which are not needed to
install Mayan EDMS. Switch over to preparestatic, which contains a
predefined ignore list.
2019-05-24 11:17:14 +02:00
Roberto Rosario
37c57056cd Add checkout details view tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-04-06 20:17:55 -04:00
Roberto Rosario
c721413209 Insert API context external object automatically
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-04-06 20:08:05 -04:00
Roberto Rosario
07ea45992b Add document indexing API tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-04-06 20:07:01 -04:00
Roberto Rosario
9708131712 Add non breakable space to avoud badge jumping
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-04-06 20:06:41 -04:00
Roberto Rosario
cdd0380f1d Merge remote-tracking branch 'origin/versions/next' into versions/next 2019-04-06 20:06:05 -04:00
Roberto Rosario
1eb9975dd6 Fix server side AJAX template rendering
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-04-06 20:05:33 -04:00
Roberto Rosario
4d8dc8e552 Fix multiple tag selection wizard step
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-04-06 20:04:39 -04:00
Roberto Rosario
97fb5f96a7 Reject email attachments of size 0
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-04-06 19:57:50 -04:00
Manoel Brunnen
d4403daa61 Workaround for pip bug #6179
See https://github.com/pypa/pip/issues/6197
2019-04-02 13:39:59 -04:00
Roberto Rosario
ff6e4294e9 Merge commit '5c9ff90d288e48d0cec78f6446fcc904df72da16' into versions/next 2019-04-02 13:39:36 -04:00
Roberto Rosario
eb6f88dfd1 Fix user management tests
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-04-02 13:39:21 -04:00
Roberto Rosario
83a4368eef Simplify document indexing test
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-04-02 13:39:00 -04:00
Roberto Rosario
b6e0de01f3 Make random PK mixin work with pre_save signals
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-04-02 13:38:21 -04:00
Roberto Rosario
bda4902bc7 Checkout manager optimization
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-04-02 13:38:02 -04:00
Roberto Rosario
5de6fbe914 Merge branch 'feature/mailing_events' into versions/next
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-21 19:56:44 -04:00
Roberto Rosario
3bbef4253a Merge branch 'features/mercs_5_6' into 3_way_merge
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-21 19:48:13 -04:00
Roberto Rosario
6dd61f187f Merge remote-tracking branch 'origin/versions/next' into 3_way_merge
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-21 19:46:22 -04:00
Roberto Rosario
d55e9c0944 Update Makefile
Add deletion of Python3 cache files to the clean target.
Allowing passing extra arguments to the test targets.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-21 19:37:08 -04:00
Roberto Rosario
862c3ff568 PEP8 style cleanups
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-03-18 04:43:31 -04:00
Roberto Rosario
a815c3f538 Fix JavaScript downloader defaults
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-03-18 04:33:36 -04:00
Roberto Rosario
54539c9d03 Update requirement versions and removals
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-03-18 04:24:20 -04:00
Roberto Rosario
2fbe4625c0 Add workflow transition API views
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-17 17:57:18 -04:00
Roberto Rosario
62c92ba6fd Add support for runtime queryset method
Allow passing runtime queryset to FilteredPrimaryKeyRelatedField
using a method name via the source_queryset_method attribute
or a default method name of "get_<field_name>_queryset".

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-17 17:53:30 -04:00
Roberto Rosario
7aa4b480d7 Fix failing ACL test
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-16 21:55:47 -04:00
Roberto Rosario
490bbee81e Fix metadata wizard step
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-16 21:55:21 -04:00
Roberto Rosario
5850ea99d4 Add workflow state API views
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-16 15:10:42 -04:00
Roberto Rosario
952380502b Complete basic workflow CRUD API views
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 19:43:54 -04:00
Roberto Rosario
97c9cfda6a Start workflow app API refactor
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 19:29:39 -04:00
Roberto Rosario
c152156a11 Refactor metadata app API
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 04:49:51 -04:00
Roberto Rosario
0c312b343e Unify BaseAPITestCase with GenericViewTestCase
Make BaseAPITestCase a subclass of GenericViewTestCase.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 03:04:17 -04:00
Roberto Rosario
7e141c1d04 Add FilteredPrimaryKeyRelatedField
FilteredPrimaryKeyRelatedField is a subclass of PrimaryKeyRelatedField
that filters its queryset by a permission.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 03:03:06 -04:00
Roberto Rosario
50d4aa0e22 Allow disabling test's expected_content_type
Setting expected_content_type to None will now disable
the reponse HTTP content type checking. Added
to allow API tests to be a subclass of the test view test
case and support all the mixins without having to declare
them separately.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 02:56:34 -04:00
Roberto Rosario
bf733be4c5 Display full trace during app loading exceptions
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-15 02:55:27 -04:00
Roberto Rosario
da2ff0bdd8 Allowing adding an additional test permission
For tests that required using two test permission, like
the tests for .restrict_queryset_by_accesses().

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-05 20:32:31 -04:00
Roberto Rosario
4b444a75cc Add support for multi access filtering
This change allows filtering a queryset by multiple permission
following a logic operator to define the relationship.

Example: In order to access an instance of MetadataTypeDocumentType
the document type view and metadata type view permissions are
required. The computation for this access control can now be
coded using .restrict_queryset_by_accesses. Custom permission
checking in the view is no longer required.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-05 20:30:26 -04:00
Roberto Rosario
378511aea3 Finish fixing failing ACL app tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-04 16:20:12 -04:00
Roberto Rosario
2a1e060907 TestModelMixin: Perform stateless model creation
Don't delete test models at the end of the test case.
Failed test cases don't execute the tearDown() method.
Instead perform model registry cleanup before creating
any new test model.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-04 16:18:39 -04:00
Roberto Rosario
2cfd4a9095 Add new ACL app API tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-03 20:02:36 -04:00
Roberto Rosario
711a28dccf Test models: Clear ContentType cache
Clear the ContentType cache when adding or removing
test models.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-03 20:01:50 -04:00
Roberto Rosario
b99cf97558 Improve REST mixins and add a new one
Add type casting to ExternalObjectListSerializerMixin via
the external_object_list_pk_type option.

Add an ExternalObjectSerializeMixin for related objects.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-03 14:31:48 -04:00
Roberto Rosario
442faca915 Update test models to generate random PK
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-02 17:32:38 -04:00
Roberto Rosario
0f84b7a723 Merge branch 'features/mercs_5_6' of gitlab.com:mayan-edms/mayan-edms into features/mercs_5_6 2019-03-02 17:10:25 -04:00
Roberto Rosario
2a67cf271e Refactor ACL app API
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-02 16:03:29 -04:00
Roberto Rosario
0cbd9e0d45 ACLs: Make get_inherited_permissions recursive
Update .get_inherited_permissions() to grab the permissions
of an object up the parent tree. Also add the role
permissions. Finally filter all the permissions by those
that apply to the object.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-02 01:51:23 -04:00
Roberto Rosario
48aad4f356 Add mixin to provide temporary test models
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-03-02 01:49:30 -04:00
Roberto Rosario
5c5979c5af Sort import
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-25 21:10:09 -04:00
Roberto Rosario
54100f7538 Role permissions API: Add permission checking and tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-25 21:09:21 -04:00
Roberto Rosario
8599d69d23 Refactor dynamic search app API
Convert the API to use viewsets.

The search function is now a service of the search model
resource.

The simple and advance search are now the same service. The
difference is determined by the URL query. A ?q= means a
simple search. For advanced search pass the search model
fields in the URL query, example: ?q=document_type__label=

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-23 05:08:39 -04:00
Roberto Rosario
23d56c3147 Improve ClientMethodsTestCaseMixin
Reduce repeated code.

Add support for passing query string dictionary to
the test client.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-23 05:04:28 -04:00
Roberto Rosario
7e1de2623c Update OCR app
Normalize API base names.

Update ViewSet base class, model classes are not needed
the OCR API views.

Split API tests into content and submit tests.

Puntuate view test strings.

Make use of success and title strings.

Make use of external object mixin in document type
settings view.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 23:49:48 -04:00
Roberto Rosario
21da6742b0 Increate default maximum title lenght to 120
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 23:48:04 -04:00
Roberto Rosario
d546967d1d Refactor the parsing app API
Add additional API and view tests.

Add success and multi document titles strings.

Make use of external mixin in the document type submit view.

Puntuate all view text strings.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 23:46:15 -04:00
Roberto Rosario
3917ca667a Fix typo in docstring
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:09:11 -04:00
Roberto Rosario
5c20a92f27 Add the RecentDocument mixin
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:07:42 -04:00
Roberto Rosario
a91bc6716d Register the Tag serializer
Register the Tag model to TagSerializer relationship
used by the events API.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:06:36 -04:00
Roberto Rosario
3fc463bb1c Fix event commit
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:06:06 -04:00
Roberto Rosario
2654c96e1c Update success message and external object usage
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:05:24 -04:00
Roberto Rosario
744d252640 Update classes and API URLs for uniformity
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:04:16 -04:00
Roberto Rosario
d74d13450c Use underscore in API resource for uniformity
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:02:52 -04:00
Roberto Rosario
b975c75c2f ContentTypeViewMixin allow chaging URL kwargs
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:01:46 -04:00
Roberto Rosario
1cb3f9fe60 Remove AJAX workers
Specified in ef415ef826.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 04:00:47 -04:00
Roberto Rosario
68c67abaa3 Improve how to get queryset from a content type
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 03:58:48 -04:00
Roberto Rosario
ef415ef826 Refactor the events app API
Use viewsets for the events app API.

Use new link badge support for the unread notification
count display.

Remove AJAX workers support now that it is now needed
anymore.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 03:57:07 -04:00
Roberto Rosario
5a8e691388 Navigation: Add support for link badges
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-22 03:56:29 -04:00
Roberto Rosario
5f264e2aae Initial refactor of the event's app API
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-20 04:21:56 -04:00
Roberto Rosario
0e524e44ed Fix failing mailer tests
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:38:09 -04:00
Roberto Rosario
023d82c96c Update document API to use new MayanAPIviewset
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:37:16 -04:00
Roberto Rosario
9370b2effb Update documents app serializers
Update serializers to be a subclass of
LazyExtraFieldsHyperlinkedModelSerializer to allow adding
more fields remotely.

Update URL fields to use MultiKwargHyperlinkedIdentityField.

Rename URL fields for uniformity.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:34:56 -04:00
Roberto Rosario
30e8327db9 Update document API sub URLs for uniformity
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:27:23 -04:00
Roberto Rosario
7eaa096ad7 Convert the success header generation to a mixin
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:25:43 -04:00
Roberto Rosario
495cd18e34 Add multiple argument support to HyperlinkField
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:25:09 -04:00
Roberto Rosario
8c3e4fa5c0 Improve Document Tag API URL
Update the API routers registration to not duplicate
the document's URL parameter definition.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:23:51 -04:00
Roberto Rosario
c2dd01d51e Refactor the OCR app API
This refactor adds two new endpoints to view the OCR
content of versions and documents.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@mayan-edms.com>
2019-02-19 03:21:36 -04:00
Roberto Rosario
e03f017e7f Remove sidebar menu from apps
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:33:42 -04:00
Roberto Rosario
0b8b3c31d2 Update DocumentSerializer to LazyExtraFields
Allow changing the fields of the DocumentSerializer
in runtime.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:28:35 -04:00
Roberto Rosario
931d31cf02 Remove sidebar menu from documents app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:27:17 -04:00
Roberto Rosario
5d149c5968 Improve tag workflow actions with transactions
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:26:46 -04:00
Roberto Rosario
d200f6d3c9 Update tag app test
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:26:18 -04:00
Roberto Rosario
5ef12555a4 Update success and title messages in views
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:25:33 -04:00
Roberto Rosario
11252ac397 Remove unused imports
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:24:55 -04:00
Roberto Rosario
1c3595c66e Add document attach and remove methods to Tag
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:24:25 -04:00
Roberto Rosario
bb7bbb299b Add document methods to attach and remove tags
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:24:05 -04:00
Roberto Rosario
070df8ae37 Remove unsed tag icon
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:23:25 -04:00
Roberto Rosario
95faa44d76 Don't make the tag selection required
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:23:00 -04:00
Roberto Rosario
cae7b8f8c5 Add document tags API views
These views allow accesing the tags list of a document as
well as attaching or removing tags in bulk.

The URLs for tag list, attach and remove are added to the
DocumentSerializer.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:22:37 -04:00
Roberto Rosario
bb6a827f28 Add LazyExtraFieldsHyperlinkedModelSerializer
This class is a mixin of LazyExtraFieldsSerializerMixin and
serializers.HyperlinkedModelSerializer. It allows adding fields
to a 3rd party app serializer without having to import the serializer.
Referencing is done using the dotted path of the serializer.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:18:33 -04:00
Roberto Rosario
863a2680a9 Fix id_list splitting
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:17:48 -04:00
Roberto Rosario
efde174b1a Add MayanAPIGenericViewSet
This viewset allow adding generic action API endpoints.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:16:30 -04:00
Roberto Rosario
6eb986f7d1 Add HyperlinkField for serializers
This field allow adding URLs to ModelSerializers.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-15 04:15:19 -04:00
Roberto Rosario
d85e838480 Add icon to workflow transition triggers
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 04:04:21 -04:00
Roberto Rosario
b546be8ea2 Remove sidebar menu from workflow app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 04:03:52 -04:00
Roberto Rosario
ba17fe742a Update success_message variable
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 04:01:37 -04:00
Roberto Rosario
5d716cd69d Add the event view link for roles
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 03:56:11 -04:00
Roberto Rosario
529ab2a6ad Tool and Setup view updates
Simplify the context methods.

Add subtitle explanations for the views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 03:02:27 -04:00
Roberto Rosario
b25c3be969 Navigation improvements
Rename the get_menu_links and get_menus_links to
navigation_resolve_menu.

Change the return value of the menu resolving to include
the resolved object.

Update the links display templates to show which object the
links belong to when there is more than one object.

Update the links display templates to show which menu
the links belong to when there is more than one menu.

Remove the sidebar menu and unify its links with the
secondary menu.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 03:02:12 -04:00
Roberto Rosario
18e5ee1e4f ACL app updates
Update the ACL permission view to use the new AddRemoveView.

Add ACL created and ACL edit events.

Add permission adding and removal accesors to the ACL model.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 02:30:51 -04:00
Roberto Rosario
6a57a5a7de Improve filtering in AddRemove View
Make sure to always used the base filtered source queryset.

Remove the grouped attribute which is subclass specific.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-14 02:27:12 -04:00
Roberto Rosario
8589004173 Add support for single or multiple objects modes
View that use the MultipleObjectMixin can now fully operate
as single object or multiple object views.

Add the self.view_mode_single and self.view_mode_multiple flags.

Add support for single, singular and plural titles and success
messages via:

success_message_single, success_message_singular,
sucess_message_plural, title_single, title_singular and
title_plural class attributes.

Insert object_list and object as attributes of the view class
to avoid calling the queryset again.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 18:07:44 -04:00
Roberto Rosario
23b1375289 Enclose add/remove tag methods in transactions
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 03:42:37 -04:00
Roberto Rosario
fb608bba98 Fix issue in ExternalObjectListSerializerMixin
Fix error when only an ID list field is specified.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 03:40:55 -04:00
Roberto Rosario
d28bb60abd Fix tag attach wizard step
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 03:40:12 -04:00
Roberto Rosario
f3f7b4bb7d Refactor the permissions app
Use the new AddRemove View for the Role's group and
permissions views as well as the Group's role views.

Convert the API to use viewsets.

Add more tests.

Add role created and edited events.

Add event subscription support to roles.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 03:36:16 -04:00
Roberto Rosario
1fee7260e4 Allow adding extra buttons to forms
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 03:35:43 -04:00
Roberto Rosario
dcd1af685a Add new AddRemoveView view
Add a new view based on AssignRemove with extra features
and filtering. AddRemoveView also has two new buttons:
Add all, Remove all.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-12 03:34:14 -04:00
Roberto Rosario
b633238610 Fix pk_list_field processing
This field was being ignored. Improved the code to check for
values in sequence.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-08 00:53:57 -04:00
Roberto Rosario
ae1634c378 Users: Finish API refactor
- Update groups add, remove and users add, remove methods trigger
only one event on the parent method and multiple on the child method.

- Add missing group_list, _add, _remove permissions.

- Monkey patch Django's User and Group model save method to
trigger the creation and edited events.

- Monkeypatch user sorting to silence warnings.

- Improve test mixins to allow reuse of view and API view
requests.

- Finish adding all API tests.

- Add events test from API view requests.

- Remove event commits from views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-08 00:44:26 -04:00
Roberto Rosario
61ebda6e63 REST API app updates
- Add back support for API views but using the
api_urlpatterns list. Needed for the current user
API until a dynamic route router is implemented that
can allow a viewset action to specify its entire URL.

- Make sure the user is authenticated before
trying to the user permissions.

- Improve how external_object_list options are read from
the class.

- None authenticated users will get a blank queryset if the
view doesn't require a permission.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-07 20:13:35 -04:00
Roberto Rosario
e4af406d5f Refactor the user management app API
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-07 20:12:55 -04:00
Roberto Rosario
ee2637dddc Common: Improve API view and tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 21:57:02 -04:00
Roberto Rosario
7d3677acfb View name cleanups
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 21:37:46 -04:00
Roberto Rosario
999e164c3d Refactor the Django GPG app API views
Convert the Django GPG app API view to use viewsets.

Add key-list API view test.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 21:36:27 -04:00
Roberto Rosario
278f97b7e4 Start tags app API refactor
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 05:20:42 -04:00
Roberto Rosario
ea3ba2c4de Complete the MOTD app API views
Add per viewset action permissions.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 05:19:31 -04:00
Roberto Rosario
627056f1ae Refactor the REST API app
Remove the APIRoot view.

Remove the Endpoint class.

Remove the EndpointSerializer.

Move API documentation generation from the root urls module
to the app's urls module.

Update the app API URL generation to be based on viewsets
instead of an custom api_urls list.

Remove MayanObjectPermissionsFilter and replace it with
MayanViewSetObjectPermissionsFilter which allows mapping
a required permission to a specific viewset action.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 05:19:07 -04:00
Roberto Rosario
7ba47d5c5f Update mailer app
Sort arguments.

Fix failing tests.

Sort view classes.

Replace get_object() with self.object in the delete and
edit views.

Use ExternalObjectMixin to simplify views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 01:08:14 -04:00
Roberto Rosario
27517c04f2 Fix ACL action tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-06 00:51:26 -04:00
Roberto Rosario
e9cdc958f6 Fix typo in link view
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-05 05:54:36 -04:00
Roberto Rosario
8284dcf306 Improve next_url and previous_url calculation
Instead of calculating these values in the dispatch
method, add new methods to calculate and insert the values of
next_url and previous_url in the context.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-05 05:50:25 -04:00
Roberto Rosario
a4ef6b3e8a Small code cleanups
Add keyword arguments.

Replace get_queryset with get_object_list.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-05 05:49:47 -04:00
Roberto Rosario
71c2a7773e Support separate sortable fields
Add support to sort a model column by a field
other than the one being displayed.

Fix the missing column issue in the list subtemplate.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-05 05:47:40 -04:00
Roberto Rosario
bd12d587ee Refactor document indexing app
Convert half the widget to HTML widgets.

Rename links and views to use the nomeclature _template_
and _instance_ to differenciate between index instances
and index templates.

Update URL parameters to use the "_id" form.

Add more tests.

Add model permission inheritance to the IndexTemplateNode,
and IndexInstanceNode models.

Remove the level and document count display from the
instance node. Display instead the total items.

Use a FilteredSelectionForm subclass to display the list
of index templates to rebuild.

Add missing icons.

Add keyword arguments to links.

Modernize tests to use the document test mixin.

Update the permission requirements for the index template
document type selection screen. The document type view
permission is now required in addition to the index
template edit permission.

Use ExternalObjectMixin to reduce the code in all views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-05 05:47:10 -04:00
Roberto Rosario
4ab2b4fee0 Merge branch 'versions/next' into features/mercs_5_6
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-03 23:48:10 -04:00
Roberto Rosario
67cd01f5ae Update permission variable name
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-03 23:44:29 -04:00
Roberto Rosario
f93ae2f395 Don't override success_url everytime
Only override success_url if self.get_post_object_action_url()
provides an alternative.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-03 23:43:34 -04:00
Roberto Rosario
0918931713 Add test mixin to generate random document types
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-03 23:43:14 -04:00
Roberto Rosario
aa95a61451 Refactor metadata app
Update permission variable name from
"permission_document_metadata_" to "permission_metadata_".

Fix failing tests.

Add test for same metadata type mixin.

Split metadata add and remove test into test for GET and
POST requests.

Remove use of urlencode and instead use furl.

Simplify view using self.action_count and
self.action_id_list.

Use ExternalObjectMixin to remove repeated code.

Move the repeated code to test for all documents to
be of the same type into its own mixin.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-03 23:37:52 -04:00
Roberto Rosario
dcea32ae38 Refactor file metadata app
Add translatable label to the label admin method.

Add access association from DocumentVersionDriverEntry to
document version.

Enclose process method and event commit in a transaction.

Update process method to not error out if EXIF tool
is not found.

Update views and tests to use ExternalObjectMixin and
comply with MERCs 5 and 6.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-02-03 19:22:49 -04:00
Roberto Rosario
4376d76c8a Load the converter class on demand
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-01 04:20:47 -04:00
Roberto Rosario
5b6a6bccb2 Add columns for duplicated document proxies
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-01 04:19:53 -04:00
Roberto Rosario
991bd9df32 Insert the external object into the view
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-01 04:19:14 -04:00
Roberto Rosario
6143cb5155 Sync list header code to row code
Add the list display code to display columns
marked as identifier.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-01 04:17:26 -04:00
Roberto Rosario
e5cd5a40c3 Improve ACL navigation
Update the ACL delete icon for uniformity.

Insert both the ACL and object in the view to also
display the ACL permissions and delete view when
viewing the ACL of an object.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-01 04:15:16 -04:00
Roberto Rosario
f92d99bd9a Refactor the converter app
Don't cache the entire converter class to lower memory usage.
Instead a get_converter_class() function is now provided to
load the converter backend class.

Add model permission inheritance to transformations to
removel custom permission checking code in the views.

User keyword arguments.

Update URL parameters to the '_id' form.

Add missing edit and delete icons. Improve the create
icon using composition.

Update add to comply with MERCs 5 and 6.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-02-01 04:00:37 -04:00
Roberto Rosario
8e66eefe7c Move file and storage code to the storage app
The setting COMMON_TEMPORARY_DIRECTORY is now
STORAGE_TEMPORARY_DIRECTORY.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 22:30:51 -04:00
Roberto Rosario
125a4317f4 Add custom DatabaseWarning
This warning is used to categorize the SQLite production usage
warning.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 22:23:23 -04:00
Roberto Rosario
0a864c2f07 Update ADMIN references to SUPERUSER
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 06:11:20 -04:00
Roberto Rosario
0919718114 Update app to use new hooks interface
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 06:10:45 -04:00
Roberto Rosario
9328a3e26e Make new version upload link smarter
Use the new document pre save hooks to disable the
new version upload link via external functions.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 06:09:11 -04:00
Roberto Rosario
cce6636b05 Improve document version hook system
Add support for new pre save hooks.

Hooks are now lists of functions instead of dictionaries.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 06:07:59 -04:00
Roberto Rosario
d5fc50272d Enable pre save hook
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 06:07:29 -04:00
Roberto Rosario
e97dde5b46 Enclose document type change in a transaction
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 05:59:11 -04:00
Roberto Rosario
495ac8d196 Object action mixin
Add post_object_action_url property to redirect the view after
all items in the queryset have been processed.

Add the exception instance in the error message context.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 05:57:44 -04:00
Roberto Rosario
3c2d2d1087 Update comment
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 05:57:18 -04:00
Roberto Rosario
e007af6b3f Refactor checkouts app
Change "checkin" usage to "check_in".

Update URL parameters to the "_id" form.

Add support to checkout and check in multiple documents.

Optimize queries that used an ID list of documents for
filtering using values_list('pk', flat=True). These
queries now use .values('pk') as a subquery.

Add pre save hooks to block new document version uploads.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 05:57:01 -04:00
Roberto Rosario
3976766abe Autoadmin: Fix failing test
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 01:20:41 -04:00
Roberto Rosario
43d79a9d86 Django settings: Add defaults, add new setting
Add support for LOGOUT_REDIRECT_URL.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 01:13:34 -04:00
Roberto Rosario
66670a5d59 Update fallback to redirect view
When there is no HTTP referer fallback to
common.settings_home_view instead of LOGIN_REDIRECT_URL.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 01:10:59 -04:00
Roberto Rosario
a06c633568 Authentication: Use class based views
Update all views to use the new Django authentication class
based views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 01:08:53 -04:00
Roberto Rosario
c61f709c1b Fix authentication app tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-31 01:05:28 -04:00
Roberto Rosario
38c4643302 Simplify RestrictedQuerysetMixin queryset return
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 17:12:01 -04:00
Roberto Rosario
65d75dafde Fix and improve test for the ACL app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 17:11:15 -04:00
Roberto Rosario
46812ab3d3 Fix ACL filtering case #3
Test case #3: Generic Foreign Key, multiple ContentTypes + object
IDs.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 17:09:46 -04:00
Roberto Rosario
4ba2d375af Update generic view and check access interfaces
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 03:54:10 -04:00
Roberto Rosario
b4a81ee0bc Random ID test mixin: Restore save method
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 03:29:12 -04:00
Roberto Rosario
08fac9fd9d Events: Update generic view interface
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 03:11:07 -04:00
Roberto Rosario
5bab080553 Workflows: Update generic view interface
Add icons for the workflow runtime proxy views.

Fix failing tests.

Convert runtime proxy links to use the new list facet menu.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-30 03:08:15 -04:00
Roberto Rosario
f65f363361 Refactor user management app
Add keyword arguments.

Update view resolutions and URL parameters to the '_id' form.

Remove code from create and edit subclasses and user
the super class error checking.

Cache the view object instead of using .get_object()
every time.

Movernize tests.

Update views to comply with MERCs 5 and 6.

Split UserTestMixin into mixins for Groups and Users tests.

Add super delete and detail tests.

Remove redundant superuser filtering from views.

Add transactions to views that also commit events.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 13:35:10 -04:00
Roberto Rosario
3bd33db023 Update serializer_string to serializer_path
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:32:57 -04:00
Roberto Rosario
b4188de727 Allow passing id_lists from POST requests
Normally the MultipleObjectMixin class view only allows
id_list from the GET request. This is updated to allow
that query from POST requests like those produced by the
view tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:30:32 -04:00
Roberto Rosario
fcfe7686fa Update document transformation links and views
Update the URL nomeclature for uniformity.

Add document transformation link tests and improve
the transformation view tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:29:27 -04:00
Roberto Rosario
a64bc61810 Allow defining SourceColumns without attributes
SourceColums that don't specify an attibute or function
will receive the instance itself instead.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:27:50 -04:00
Roberto Rosario
da638dc7f9 Sort class property
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:27:25 -04:00
Roberto Rosario
b5839c0662 Refactor the tags app
Remove the widget from the model.

Add keyword arguments.

Separate form widgets from html widgets. HTML widgets now go
in the html_widgets module.

Update the TagMultipleSelectionForm class to be a subclass of
FilteredSelectionForm.

Move Select2 specific JavaScript from the appearence app to the
tags app.

Update tag attachment and removal view names.

Modernize tests.

Add more tests.

Consolidate repeated test code into test mixins.

Update views to comply with MERCs 5 and 6.

Use uniform nomeclature for URLs.

Update URLs parameters to use the '_id' form.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:20:54 -04:00
Roberto Rosario
ef5e0c2d86 Remove last usage of .filter_by_access()
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-29 04:18:02 -04:00
Roberto Rosario
fbb3a64bce Update check_access interface
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:40:22 -04:00
Roberto Rosario
c09b58894b Update views to import from common.generics
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:39:44 -04:00
Roberto Rosario
eae5359cdf Remove the old check_permissions implementation
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:36:37 -04:00
Roberto Rosario
27546dadd9 Navigation: Update ACL interface
Update the check_permission interface usage.

Use the model's default_manager instead of the explicit
.objects manager.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:35:24 -04:00
Roberto Rosario
74dfa53787 Update documents app
Rename the DeletedDocument proxy model to a TrashedDocument.

Rename the deleted_document views to trashed_document.

Rename the document and deleted_document URL parameters to
trashed_document.

Update URL parameters to the '_id' form.

Add keyword arguments.

Update use of .filter_by_access().

Enclose trashed document restore method in a transaction.

Sort arguments.

Update app for compliance with MERCs 5 and 6.

Add document page view tests.

Add favorite document view tests.

Movernize tests.

Replace use of urlencode with furl.

Update views to use ExternalObjectMixin.

Refactor the document and version download views.

Rename the DocumentDocumentTypeEditView to DocumentChangeTypeView.

Move the trashed document views to their own module.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:25:48 -04:00
Roberto Rosario
7532429b0b Refactor common generic views
Add keyword arguments.

Sort arguments.

Unify the ObjectListPermissionFilterMixin and
ObjectPermissionCheckMixin into the RestrictedQuerysetMixin.

Add MultipleObjectDownloadView.

Update SingleObjectDownloadView to do queryset filtering.

The method that returns the base queryset for views is
now named get_source_queryset().

The views now use .get_object_list as a multi object
homologous of get_object. The queryset returned by
.get_object_list is restricted by access.

Make MultipleObjectMixin a subclass of Django's
SingleObjectMixin to reduce repeated code.

All generic views are now imported from common.generics and not
from common.views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 05:18:33 -04:00
Roberto Rosario
9261b6e687 Remove deprecation comment
With the removal of the support for a related field in
.restrict_queryset() the deprecation comment can now be
removed.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-28 04:52:05 -04:00
Roberto Rosario
33e0e694e3 Smart settings: Remove the 'is_path' argument
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:40:29 -04:00
Roberto Rosario
4937d8b776 Update document signatures app
Add keyword arguments.

Remove source column functions and move their code to the model.

Use the FilteredSelectionForm for the key selection in the
document version signing view.

Update the field definition of the DetailForm subclasses
to use the new internface.

Update URL parameters to use the "_id" form.

Update views filtering to comply with MERC 6.

Move repeated test code to its own test mixin.

Update links to work with the new Link class interface.

Modernize tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:32:05 -04:00
Roberto Rosario
890f872681 Add keyword argument
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:30:05 -04:00
Roberto Rosario
9ce930367d Remove use of object_related view attribute
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:29:23 -04:00
Roberto Rosario
746f40dda0 Add missing line in introspect_attribute
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:26:21 -04:00
Roberto Rosario
319b74c85f Force use of get_object_list method
Update the SingleObject Delete, Detail and Download views
to force use of a get_object_list method instead of allowing
subclasses to override the get_queryset method and bypass
the object permission checks.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:24:54 -04:00
Roberto Rosario
2ed7858acb Move filterted from initialization
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:24:19 -04:00
Roberto Rosario
c5d4054fb6 Add test mixin to generate random primary keys
Add a new mixin to monkey patch the Model class to
force each newly created model instance to use a randomly
generated primary key.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:22:57 -04:00
Roberto Rosario
382995ae40 Update ACLs app
Remove support for passing a related field argument when
checking for access for restricting a queryset.

Remove a duplicate permission check.

Fix bug when filtering the direct ACL for an object,
the ACL query was filtering by the ACL ID instead of the
object ID.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:18:44 -04:00
Roberto Rosario
f076a49d2d Deprecate the check_permissions method
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:17:21 -04:00
Roberto Rosario
c5ce20bbea Remove role permission grant revoke permissions
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:16:48 -04:00
Roberto Rosario
9203977261 Update all links to the new Link class interface
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:13:53 -04:00
Roberto Rosario
daf79983aa Update Link class interface
Remove Link class support for multiple permissions. Accept
only one permission for each link. Remove support for the
permission related field.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-25 01:11:51 -04:00
Roberto Rosario
75fd7647d4 Keys: Update use of DetailForm
Fix absolute URL keyword argument.

Move detail generation to the model.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-23 14:49:59 -04:00
Roberto Rosario
8c085331f1 DetailForm: Use Meta class instead
Instead of class attributes, make a generic reusable the
FormOption class and update the DetailForm to use a Meta
class for options.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-23 14:48:23 -04:00
Roberto Rosario
3f48a5549e Sort source form definitions
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 22:38:37 -04:00
Roberto Rosario
c059f1f021 Fix the cabinet wizard step
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 22:37:24 -04:00
Roberto Rosario
1d0ebbab64 FilteredSelectionFormOptions updates
Fix displaying the name of the subclass when the
queryset is missing.

Add support for passing a new argument to specify
if the field is required or not.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 22:35:37 -04:00
Roberto Rosario
a769cc92e3 Fix staging file delete view
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 22:21:49 -04:00
Roberto Rosario
205ca594f5 Replace filter_by_access with restrict_queryset
With the interface finalized, replace .filter_by_access() in
the generic view mixins with restrict_queryset().

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 22:16:56 -04:00
Roberto Rosario
108c54630f Update source app
Sort arguments.

Add keyword arguments.

Update views regexes.

Update URL parameters to use the "_id" form.

Move setting literals to the literals.py module.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 04:27:00 -04:00
Roberto Rosario
c7bd2ee8f2 Update document states app
Change the app view namespace from 'document_states' to
'workflows'.

Add missing icons.

Improve view names.

Split views into 3 modules: workflows views, runtime proxy views
and instance views.

Update views to comply with new MERCs 5 and 6.

Fix failing tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-22 03:19:30 -04:00
Roberto Rosario
55356c4781 Update document state app
Sort arguments. Add keyword arguments. Update URL parameters to
the '_id' form.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-21 20:07:40 -04:00
Roberto Rosario
83a9b5a60a Update OCR app
Add keyword arguments. Update URL parameters to the "_id" form.
Updated view tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-21 19:24:00 -04:00
Roberto Rosario
50333d1326 Update smart settings app
Sort arguments. Add keyword arguments.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 04:06:31 -04:00
Roberto Rosario
ad7c77b4f3 Update dynamic search app
Sort methods. Update use of .filter_by_access() to
.restrict_queryset().

Change the method to so the final object
filtering. Instead of expressing the pk list and remove the
duplicated using a set, pass the queryset as a subquery to
the object filter. This moves the processing to the database
instead of holding a list of an unknown number of primary
keys in the memory.

Add keyword arguments.

Update tests to use the latest user test case mixin interface.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 03:53:55 -04:00
Roberto Rosario
166183dff9 Update metadata app
Sort arguments. Add keyword arguments. Update URL parameters
to the '_id' form. Remove use of .check_access() from views.
Sort methods.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 03:31:19 -04:00
Roberto Rosario
09edab5027 Update lock managet app
Add keyword arguments. Sort imports. Move settings and test
literals to their own module.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 02:50:34 -04:00
Roberto Rosario
027a853885 Update events app
Add keyword arguments. Update URLs for uniformity.
Update URL parameters to the '_id' form. Update
views to remove use of .check_access(). Fix escape
sequence warning in migration 0005.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 02:37:45 -04:00
Roberto Rosario
2e5d05403a Update linking app
Add keyword arguments. Update URL parameters to the '_id' form.
Movernize tests and update them to use the latest test case
improvements.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 02:00:22 -04:00
Roberto Rosario
c0b34067ef Update document parsing app
Update URL parameters to the "_id" forms. Add keyword arguments.
Remove use of is_path in the DOCUMENT_PARSING_PDFTOTEXT_PATH
setting.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-21 00:31:06 -04:00
Roberto Rosario
fc29309f68 Update Django GPG app
Add keyword arguments to all calls. Rename URL parameters to be
explicit ("key_id"). Add key delete view test. Update tests
to use a mixin for repeated key creation code. Grant permissions
and access the proper way using self.grant_permission and
self.grant_access.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-20 18:08:47 -04:00
Roberto Rosario
14fd5f02a8 Remove unused code from events app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 04:22:27 -04:00
Roberto Rosario
622972fd85 Update file metadata app
Add keyword arguments to links and test views.

Update URL parameters to use the _id form.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 04:21:28 -04:00
Roberto Rosario
6376445cc4 Update document comments app
Add keyword arguments to the app links.

Remove use of `raise_404`.

Update URL parameters to use document_id and comment_id.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 04:08:45 -04:00
Roberto Rosario
53f3261dae Fix keyword argument name
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 03:56:56 -04:00
Roberto Rosario
79742e82f9 Add missing logger instance
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 01:21:26 -04:00
Roberto Rosario
3f97bc1a68 Update ContentTypeSerializer URL arguments
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 01:21:04 -04:00
Roberto Rosario
a15f0b7641 Improve FilteredSelectionForm
Improve the configuration process of the FilteredSelectionForm form
by adding Meta child class support. The child Meta class
is defined in FilteredSelectionFormOptions.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 01:20:18 -04:00
Roberto Rosario
383d0fcc38 Remove support for raising 404
Remove explict support for raising 404 error when the
object access fails.

The new method to use is to restrict the queryset using
the .restrict_queryset manager method and then .get() the
desired object. If the object access control failed then
the desired object will not be found in the queryset
and an error 404 will be raised. The end result is the same:
error 404, the method to raise the error is what differs now.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 01:12:39 -04:00
Roberto Rosario
7c4ae1aef0 Update common app API to viewsets
Update the API entries for content types and templates to use
viewsets and the new api_router_entries URL registraion
method.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 01:04:59 -04:00
Roberto Rosario
16d8fb9fea Modernize MOTD app
Update API code to use viewsets. Update links and URLs to use
keyword arguments.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 01:00:58 -04:00
Roberto Rosario
9ed93b54af Add get_related_field utility function
Add the get_related_field function to resolve a
model's related field reference by a path separate
by Django's default field separator '__'.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 00:15:56 -04:00
Roberto Rosario
2d9aca55c5 Add a central module to define project warnings
Add the mayan.apps.common.warnings module with an
initial InterfaceWarning warning class used to mark
use of deprecated internal interfaces.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 00:10:40 -04:00
Roberto Rosario
354ea434ae Add keyword arguments to the ACLs app code
Rename all instance of `pk` or `acl_pk` to `acl_id`
to match the preferred URL parameter naming conventions of
using `id` instead of `pk`.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 00:09:09 -04:00
Roberto Rosario
5d7f810477 Refactor the access control computation
Rewrite the ACL queryset filtering to move most of the
computation to the database manager view the ORM.

Add support for cascading access control checking.

Update the .check_access() method to work as a front
end of the new .restrict_queryset method. The workflow
for access control now follow Django convention of
first generating a queryset and then attempt to .get()
the desired element of the queryset.

This update also allows restricting a queryset by related
fields which can be Generic Foreign Keys.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-19 00:05:21 -04:00
Roberto Rosario
b53c026877 Sort arguments and imports
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-14 00:03:26 -04:00
Roberto Rosario
097ac7dae6 Move permission purge code
Move the code to purge obsolete permissions from the management
command to the StoredPermission default manager.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-13 23:58:08 -04:00
Roberto Rosario
0e800dc314 Use keyword arguments in the permissions app
Additionall rename the views GroupRoleMembersView,
SetupRoleMembersView, SetupRolePermissionsView to
GroupRolesView, RoleGroupsView, RolePermissionsView.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-13 23:23:18 -04:00
Roberto Rosario
38d7b7cda3 Add check_permissions replacement
Add a new class method named check_user_permission.
This method is smaller as it only accepts a single permission
instead of a single or a list of permission like check_permissions
does. check_user_permission is meant to replace check_permissions.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-13 22:59:59 -04:00
Roberto Rosario
9d8c8f4833 Optimize permission check
Convert the user permission check from a double Python loop
to a single ORM query.

Add methods to the Role model to grant or revoke permissions.

Rename the method requester_has_this to user_has_this for clarity.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-13 22:57:59 -04:00
Roberto Rosario
da1d32f6cd Rename common app migration
Rename migration 0011_auto_20181229_0738 to 0012_auto_20181229_0738
to avoid conflict with an existing 0011_* migration.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-09 13:09:39 -04:00
Roberto Rosario
8f24b2ed80 Improve Template hash calculation
Calculate the Template hash from the content actually returned.
Remove the newlines as these are irrelevant for HTML.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-09 12:38:38 -04:00
Roberto Rosario
58e38c1ff9 Improve FilteredSelectionForm
Improve the configuration process of the FilteredSelectionForm form
by adding Meta child class support. The child Meta class
is defined in FilteredSelectionFormOptions.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-09 12:24:09 -04:00
Roberto Rosario
65ccbd3b7b Reorganize reusable test code
Extract test views and user code into their own separate test case
mixins. Append TestCase to test case mixins with base test code
to differentiate them from test mixins with reusable view calls.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-03 14:49:48 -04:00
Roberto Rosario
c6aab93f98 Initial audit of the document index app
Add keyword arguments to calls. Sort methods and parameters.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-02 22:53:52 -04:00
Roberto Rosario
b0e3b82755 Merge branch 'versions/next' of gitlab.com:mayan-edms/mayan-edms into versions/next 2019-01-02 22:52:44 -04:00
Roberto Rosario
3dc8df46b9 Initial audit of the document index app
Add keyword arguments to calls. Sort methods and parameters.

Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-02 19:16:32 -04:00
Roberto Rosario
cdb29b11f9 Add keyword argument
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 14:46:41 -04:00
Roberto Rosario
924538fe48 Initial audit of the convert app
Add keyword arguments to call. Sort methods and arguments.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 14:45:48 -04:00
Roberto Rosario
125c133334 Audit common app
Add support to override settings of the FilteredSelectionForm
via subclass attributes. Add keyword arguments to calls.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 14:34:41 -04:00
Roberto Rosario
92e615ce4c Add keyword arguments to checkouts app
Add keyword arguments to calls and view parameters. Add missing icons.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 14:19:32 -04:00
Roberto Rosario
3e53ce0c43 Add keyword arguments to the cabinet app
Modernize tests. Use the FilteredSelectionForm in the view
to add new cabinets to documents. Add missing icons.
Rename some view names to be more consistent.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 13:54:58 -04:00
Roberto Rosario
39689e2a4f Cleanup autoadmin app code
Add keyword arguments. Modernize view tests by using
GenericViewCase class.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 03:46:14 -04:00
Roberto Rosario
57d0bba0fa Add keyword arguments to authentication app
Modernize view tests by using self.<method> instead of
self.client.method. Reduce repetition of reverse method with literal
view name usage.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 03:35:46 -04:00
Roberto Rosario
c332fa4538 Add keyword arguments in the appearance app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 03:06:11 -04:00
Roberto Rosario
a77528862f Sort imports of ACLs app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 03:05:04 -04:00
Roberto Rosario
b3a781d51a Add ContentTypeViewMixin, ExternalObjectViewMixin
Add a mixin to ease the amount of code and imports required
for views that extract the content type from URL parameters.

Improve ExternalObjectViewMixin by adding a new class attribute
"external_object_pk_url_kwargs" to mechanize URL parameter
extraction. The external_object_pk_url_kwargs maps model
attributes using during manager get or filter from URL parameters.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 02:56:19 -04:00
Roberto Rosario
da4e4d0b46 Add duplication check to SingleObjectCreateView
Add an extra step before creation of the instance to validate
for duplication. Add the error_message_duplicate class
attribute to allow customization of the error message.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 02:53:50 -04:00
Roberto Rosario
dfd548bf62 Update ACL app to compy with MERC 5 and 6
Update the entire with keyword arguments. Update the views
to comply with MERC 6 by returning error 404 on access
failure. API are untouched. Add icon to the ACL delete
button. Add additional view tests. Use the new filtered
choice form to display a select2 enabled role selection
widget. Update the ACL creation view to not redirect to an
existing ACL in case of duplication but to instead stop
and display an error with a suggestion to the user
to instead edit the existing ACL.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 02:48:19 -04:00
Roberto Rosario
86b0463a38 Update the DisableableSelectWidget widget to work
Update the class to the Django 1.11 widget interface.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 02:46:08 -04:00
Roberto Rosario
8e0a2bbdbc Move the base test ACL mixin to the ACLs app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 02:44:50 -04:00
Roberto Rosario
2cbac826d4 Add a reusable filtered choice form
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-01-02 02:39:24 -04:00
Roberto Rosario
7f3b28aec8 Update mailer app to comply with MERCs 5 and 6
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-31 04:02:15 -04:00
Roberto Rosario
35ef8ba7b8 Update documents app to comply with MERCs 5
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-31 02:55:43 -04:00
Roberto Rosario
ccd935d752 Update tags app to comply with MERCs 5 and 6
Addionally the permission workflow is updated to work in a
reciprocal manner. In order to attach a tag, the user's role
will need the tag attach permissions for both, the document
and the tag.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-31 00:18:37 -04:00
Roberto Rosario
5365ed4fed Update status and assign number to MERCs 5 and 6
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-31 00:14:35 -04:00
Roberto Rosario
5cb674b7ab Allow external object permission via function
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-30 16:26:42 -04:00
Roberto Rosario
ffeb580c15 Add event tests to document comments app
Switch view to return an HTTP 404 on lack of authorization
instead of an HTTP 403.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-30 16:25:32 -04:00
Roberto Rosario
45ceab024d Add two new MERC proposals
Add the explicit arguments and lower information disclose MERCS.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-30 14:14:44 -04:00
Roberto Rosario
cd9d51db9e Improve document comment app
Add keyword arguments to URL definitions and reverse resolution.
Raise HTTP error 404 instead of 403 to reduce the information
divulged. Add view tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-30 02:26:23 -04:00
Roberto Rosario
82651ff32c Improve and fix failing metadata tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-29 20:43:33 -04:00
Roberto Rosario
23a4a56aae Fix failing tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-29 04:47:40 -04:00
Roberto Rosario
c40e0c136a Add group create and edit events
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-29 04:06:11 -04:00
Roberto Rosario
000fe87c37 Remove use of storage wrappers
Use a dynamic subclass instead that always deconstructs to a fake
subclass with a __eq__ method that always returns True. This should
trick makemigrations into never creating a new migrations for
changes to the storage class or the arguments.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-29 03:45:56 -04:00
Roberto Rosario
442bf5dc4b Wrap storages
Wrap storages directly connected to file model fields to avoid
Django triggering a migration change when an attribute of the
storage is changed, like the location.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-28 00:22:49 -04:00
Roberto Rosario
f96057b0fd Add more new icons
Add new icons for the apps: Document comments, documents, file metadata,
parsing and OCR.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-27 05:45:01 -04:00
Roberto Rosario
e687430cf0 Use new icon classes, improve URLs layouts
Use the new icon classes to add custom icons. Improve
the URL schemes of some apps for uniformity.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-27 04:46:56 -04:00
Roberto Rosario
24066c494e Better menu restore, add hover, active class
Improve the code that restores the menu state after a refresh.
Add hover styling for menu sub links. Add an active class
for the menu sub links and JavaScript to assign it to the
link clicked or restored.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-27 04:43:48 -04:00
Roberto Rosario
945eed7ad5 Add two new icon classes
Add new icon classes based on Font Awesome that support
layering, masking and combining glyphs.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-27 04:42:06 -04:00
Roberto Rosario
0ee82e9efe Add support for SourceColumn label display
Update the class to disable displaying the column label by default
and only so when the include_label argument is True.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-27 00:33:33 -04:00
Roberto Rosario
8d3f26bd7f Tweak panel highlight style
Increase the width of the border for hightlighted panels.
Add a hover shadow to selectable panels.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 17:30:10 -04:00
Roberto Rosario
eeceb52c06 Fix failing source tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 06:09:48 -04:00
Roberto Rosario
57269ca7f9 Improve request object resolution
Add an additional method to obtain the request when it is
not available from the context.

Add support for SourceColumn resolution of inherited
sub models.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 06:08:46 -04:00
Roberto Rosario
c9ce90ea31 Improve source column definitions
Update usage of SourceColumns in the sources app.
Move some colum definitions from the view to the app
module using SourceColumn instances.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 06:03:03 -04:00
Roberto Rosario
50ea0c15df Support list mode in staging folder source
Add no-result content to display when there are no files
in the staging folder.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 06:00:21 -04:00
Roberto Rosario
5a90d76005 Make list mode code its own mixin
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 05:59:31 -04:00
Roberto Rosario
43691de6b7 Don't override list view mode in cabinet view
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 05:58:53 -04:00
Roberto Rosario
fe2cf70d93 Display action dropdown only if there are links
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 05:58:26 -04:00
Roberto Rosario
49c9fbbce1 Remove unused file
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 02:40:07 -04:00
Roberto Rosario
4ff9794286 Update and move JavaScript install code
Update the JavaScript dependency installation code to handle scoped
packages. The code is also updated to use pathlib's Path.

Move the JavaScript dependency installation to its own app named
dependencies.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-26 02:15:44 -04:00
Roberto Rosario
d6c7a0d765 Update renamed template variable
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-25 04:55:55 -04:00
Roberto Rosario
5e4cbbe2bc Remove MultiItemForm
Remove the HTML based MultiItemForm and use a Bootstrap dropdown
menu for the bulk actions list.

Add a text message explaining that items must be selected in
other to activate the bulk action menu.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-25 04:53:06 -04:00
Roberto Rosario
9e4ebf4e04 New sidebar layout
Update UI code to display a fixed location sidebar. The sidebar is
also of a fixed width instead of being a certain number columns
wide. The action dropdown is also now in a fixed location.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-25 03:39:22 -04:00
Roberto Rosario
bd194a70cb Add spaceless tags
Add spaceless tags to the invalid document template.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 19:18:54 -04:00
Roberto Rosario
6dcd8bd9aa Add a template list API view
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 19:18:36 -04:00
Roberto Rosario
2c3e2e2bef Shorten and reorganize menu names
Change the left hand side menu designation to be the 'main'
menu from 'sidebar', the menu at the top goes from 'main'
to 'topside'. All menus are referenced by their name only
and the fragment 'menu' is dropped from all as it is obvious
that they are menus and the Menu class doesn't supply any
other kind of object.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 19:15:20 -04:00
Roberto Rosario
2402668e16 Restore the side bar menu state after a refresh
Add code to detect the current URL and open the parent of
the menu entry that correlates to it.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 18:51:53 -04:00
Roberto Rosario
38228b4fe8 Control the height of thumbnails in table view
Since tables row height can't be controlled using CSS (they
will always resize to the size of their elements), this commit
adds a table cell container whose size can be controlled from
the view. This way big thumbnail images won't break the appearance
of the table.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 14:29:14 -04:00
Roberto Rosario
91465ef9b0 PEP8 cleanup
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:31:20 -04:00
Roberto Rosario
ff24e17eb6 Expose Separator and Text for easier imports
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:26:05 -04:00
Roberto Rosario
d687e62106 Unify and remove events view and widgets
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:25:35 -04:00
Roberto Rosario
15bec5fcdb Add/Remove: Enable double click, add help text
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:23:41 -04:00
Roberto Rosario
9be8f02829 Make sure the ChoiceForm is full height
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:23:06 -04:00
Roberto Rosario
fd7e937cef Move current user views and add user events
Move the current user detail and edit views from the common app
to the user_management app. Add the user created and edited events.
Add an user detail view.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:21:25 -04:00
Roberto Rosario
931b17a447 Display facets and object list facet links
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:15:23 -04:00
Roberto Rosario
32fb40548a Display resolved attribute URL or fallback
Update template code to display the resolved attribute's absolute_url
and if it doesn't provide it, fallback to display the main object's
absolute_url.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 04:12:39 -04:00
Roberto Rosario
b848737515 Render the resolve attribute not the literal
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 02:45:05 -04:00
Roberto Rosario
4a0e9ffa15 Remove document_link widget
The SourceColumn class has now the ability to render a link's
absolute_url, turn this on for the document parsing error and
OCR error list columns and remove the document_link widget.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 02:43:26 -04:00
Roberto Rosario
ae22e0f70c Common: Small system menu fixes
Add a separator under the setup link in the system menu.
Fix the user menu separator which is at the top of the username
and should be udner it.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 01:36:20 -04:00
Roberto Rosario
012c027994 Events: Improve and merge views
Merge the current user event, the user events and the object events
views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-24 01:34:42 -04:00
Roberto Rosario
c1c8d1dc2d Converter: Add transformation edit view tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-23 21:28:05 -04:00
Roberto Rosario
25edc73100 Add support for sortable columns
Add a new argument to the SourceColumn class to flag a column as
sortable. The SourceColum will generate a request querystring to
be used as the table header archor href. The SingleObjectListView
will capture the querystring and call the order_by on the queryset
to sort it.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-23 05:11:14 -04:00
Roberto Rosario
36101dfea6 Documents: Improve slice of recent document list
Improve the method of slicing the document list to produce
a queryset that can be further sorted.

Add a new date added column to the recently added document list.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-23 02:04:54 -04:00
Roberto Rosario
28cc228b5a Apperance: Remove obsolete variables and a filter
Remove the variable multi_select_item_properties and the
get_encoded_parameter custom filter.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-23 02:02:07 -04:00
Roberto Rosario
64e1c6bb67 Add widget support to SourceColumn
Allow passing a widget class to SourceColumn. This makes
using lambdas to render model column unnecesary and are
mostly removed too.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-22 05:35:31 -04:00
Roberto Rosario
360e756093 Disable user groups link if superuser or staff
Disable the link to view an user's (or current user)
group list if the user is a superuser or staff.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-22 02:04:46 -04:00
Roberto Rosario
b79c168dab SourceColumn label optimization
Compute the SourceColumn at definition instead of doing it
during the resolve method. This move the label calculation
from the rendering to the startup.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:53:17 -04:00
Roberto Rosario
14f31d5614 Add new list templates toolbar
Add the new generic list and generic list items toolbar which allow
switching the list display mode.

This toolbar also includes a stylized button to select and deselect
all items emulating the check-all checkbox.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:51:20 -04:00
Roberto Rosario
9784798118 Tweak the vertical spacing of the viewport
Update the CSS to prefer a margin to the current padding.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:49:24 -04:00
Roberto Rosario
8ca6c563bc Update the invalid document template
Update the invalid document template to use the new font awesome
defitions.

Increase the size of the default template.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:48:26 -04:00
Roberto Rosario
213f3c1fb4 Cleanup SourceColumn invocations
Update the code of some SourceColumn invocations to be model methods
instead of lambda wapped functions.

Move the translated labels to the models too.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:47:14 -04:00
Roberto Rosario
5623f0b3a4 Improve the server error template appearance
Add a red alert for faster visual scanning. Place the
status code output in the same line as the initial message to
save some vertical space for debug output.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:44:19 -04:00
Roberto Rosario
79cba7abe1 Improve the resolve_attribute function
Update the arguments of the function to be full length and more explicit.
Use exceptions to find the correct way of using the attribute of the
object passed instead of trying to use introspection.
Add support for passing key word arguments to the attribute being
resolved even if it is a class method.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 23:41:38 -04:00
Roberto Rosario
9bcaf1849b Appearance: Update fontawesome version
Update fontawesome to version 5.6.3 and install it via NPM.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-21 20:03:44 -04:00
Roberto Rosario
c2fc10c344 Highlight selected panels
Add event handler and style sheet to highlight panels when selected.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-20 20:54:54 -04:00
Roberto Rosario
263d646c7c Appearance: Select card by clicking on the card
Add support for selecting a document from the UI by just clicking
on the title of the body of the card, not just on the checkbox
next to the title.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-20 20:40:26 -04:00
Roberto Rosario
bd54877e0c Add missing file
An __init__.py file was missing from commit
76853147c8.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-20 16:47:52 -04:00
Roberto Rosario
60ac63ead4 Add new sidebar main menu
Add a left side menu navigation style. The main app navigation links
will be displayed here. The notification, user and system tools are now
displayed at the top navigation bar.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-20 16:47:02 -04:00
Roberto Rosario
f77f64cc71 Source: Change source test behavior
Update sourcs to accept a test argument to their check methods.
This is to allow for explicit test behavior like running the
check method code even when the source is disabled and to
not deleted downloaded content during a test.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-18 17:27:40 -04:00
Roberto Rosario
7672aca7a9 Add button widget for forms
This widget will allow displaying HTML anchor buttons inside
the forms. It uses by default the navigation instance template
from the navigation app.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-18 17:21:21 -04:00
Roberto Rosario
76853147c8 Split sources models into separate modules
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-18 03:23:23 -04:00
Roberto Rosario
f71ca8f2f5 Split sources models into separate modules
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-18 03:22:26 -04:00
Roberto Rosario
ba48a7e0fd Initial implementation of mailer events
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-16 04:15:31 -04:00
Roberto Rosario
798446f362 Add klass argument to get_object_or_404 usage
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-16 01:55:33 -04:00
Roberto Rosario
727d2ecd71 Convert the title calculation into a template tag
Convert the calculate form title template into a template tag.
The result of the template tag is applied as the title property
of the <H3> HTML tag allowing users to view the full title on
mouse hover if the title was truncated.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-16 01:09:32 -04:00
Roberto Rosario
3a6a250d1b Move dashboard code to its own app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-16 00:34:21 -04:00
Roberto Rosario
6ed18926cc Move appearance templates to their apps
Move the authenticaton and navigation templates to their respective
apps.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-16 00:14:15 -04:00
Roberto Rosario
c2f10fd38d Styling: Unify styling of signal handlers
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-15 23:57:33 -04:00
Roberto Rosario
1d1600c5dd Improve the partial navigation error reporting
Add a HTTP status code display. If status code is 0 assume
there is a communication error and display such.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-15 21:02:54 -04:00
Roberto Rosario
b83ab1b528 Improve how settings are loaded from config file
Update the way settings are loaded. Instead of loading the
entire config file now settings are loaded from the config
file on demand when the cache misses.

Improve the smart settings classes tests and add another test
for the config file loading.

Add support for variable config file path.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-15 21:00:51 -04:00
Roberto Rosario
8559565dca Signatures: No terminal raises NeedPassphrase
Update the Key model's sign_file method to raise the NeedPassphrase
exception when GPG has no terminal available and not just when the
GPG backend specifically asks for a passphrase.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-15 15:27:26 -04:00
Roberto Rosario
77468a87be Tests: Fix failing tests after last refactor
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-15 15:26:19 -04:00
Roberto Rosario
0e86f2ad8a Refactor the model accesors
Refactor the accesors to behave like methods instead of properties.
This means all accesors will be prepended with the string
"get_" and will include a set of parenthesis.

Improve the ModeAttribute class to use the method's
short_description. This commit also adds support for a
new method .help_text attribute has been added.

Move accessors to their own module, named "methods.py".

Remove the PropertyHelper class as the accessors no longer
need it.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-15 04:49:40 -04:00
Roberto Rosario
8c63ef4c69 Styling: PermissionNamespace keyword arguments
Add keyword arguments to all called instances of PermissionNamespace.
Sort name and label arguments.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 21:58:38 -04:00
Roberto Rosario
3adb9d4ea0 Sort method arguments
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 18:08:44 -04:00
Roberto Rosario
4d46ca3343 Unify document type selection forms
Merge all document type selection forms into a single smarter form
that can perform permission filtering and allow single or multiple
selections.

This commit also add the document type selection for submit
view to the file metadata app.

This commit also updates the document type selection views
for the OCR, document parsing, and upload wizard to use
the new document type selection form and removes their
respective document type selection forms.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 18:06:23 -04:00
Roberto Rosario
315e70309b Documents: Split monolith forms.py
Split the documents/forms.py into sub modules for each logical
unit: types, document, pages, versions.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 15:44:25 -04:00
Roberto Rosario
34443a715c Tests: Remove unused override_settings
Now that the automatic OCR, parsing and file metadata processing
are turned off by the test setting file, these overrides in the
tests are not needed anymore.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 03:12:42 -04:00
Roberto Rosario
46c2192d9a Mirroring: Fix failing tests
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:54:54 -04:00
Roberto Rosario
108744cdd5 Styling: Remove extra semicolon from line
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:45:52 -04:00
Roberto Rosario
15180e95bf Styling: Add keyword arguments to add_to_class usage
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:44:50 -04:00
Roberto Rosario
503af584d5 Migrations: Remove superfluous code comments
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:35:24 -04:00
Roberto Rosario
b28281be5a Tests: Turn on parsing and OCR
The base test file now turns off OCR, parsing and file metadata
processing. Apps that rely on those must now turn them on
explicitly.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:24:56 -04:00
Roberto Rosario
feb92a105f Add exiftool to documentation and the Dockerfile
This binary is required by the new file metadata app.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:24:06 -04:00
Roberto Rosario
0a7908baca File metadata: Add file metadata app
Add the file metadata app. This app uses binary wrappers called drivers
to extract properties from the file of documents. The default driver
uses the exiftool to extract the EXIF record from JPEG images.
The exiftool can also extra some properties from other files like
PDFs, office files and sound file.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:16:38 -04:00
Roberto Rosario
1efec6bd41 Navigation: Related field support to SourceColumn
Add support to the SourceColumn class to resolve related fields
using the double underscore as separator. Columns that use related
no longer have to use throw away lambdas.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-14 02:15:49 -04:00
Roberto Rosario
68995adb7f AJAX: Improve error display during debugging
Add message body display when the Django debug flag is True.
Add a CSS to simulate the appearence and legibility of the
debug message.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-13 00:21:35 -04:00
Roberto Rosario
2b52ee11b2 Documents: Fix typo in UUID field help text
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 23:20:33 -04:00
Roberto Rosario
5d944b922f Appearance: Remove markup and reuse template
Remove markup in the base.html template to render the Actions dropdown
and use instead the built in navigation/generic_navigation.html
template.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 23:19:04 -04:00
Roberto Rosario
ae2205fe30 Documents, Signatures: Add icons
Add icons to document version action links. Add icons to
detached and embedded signing action links.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 23:18:04 -04:00
Roberto Rosario
84e78f16d9 Appearance: Remove fadeIn animation
Remove the fade in animation in document thumbnails. Tweak
the match height refresh interval to reduce scrollbar
jitter in Firefox. Change the fancybox display animation from
zoom to fade in.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 23:14:43 -04:00
Roberto Rosario
e83f6e55a0 Permission: Permission reference to property
Convert the volatile permission reference in the stored permission
model from a method to a property to allow future caching optimization.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 23:12:52 -04:00
Roberto Rosario
bc816ccdda OCR: Turn off parsing in OCR tests properly
The document parsing was being turned off in the OCR tests
by setting the binary to an invalid value. A proper way
to disable automatic parsing was added in a previous commit
and this commit updates the test case class to use that method.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 21:06:58 -04:00
Roberto Rosario
c7dec2ee09 Documents: Add missing migration for model rename
Add the migration for the DocumentPageResult model rename in
commit 2d4a710999.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-12 21:05:50 -04:00
Roberto Rosario
f8c25af796 Tests: Modernize some test cases
Update some view tests in the tags, sources and linking apps
to use the test case classes provided by the common app.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-09 17:10:27 -04:00
Roberto Rosario
e048f31f85 Workflows: Use full path reference to tags widget
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-09 15:10:41 -04:00
Roberto Rosario
278fdc3c9a Workflows: Don't capture form exceptions
Instead of capturing the form save exceptions in the subclass
let the base class do the work.

Use the base class get_instance_extra_data method to avoid
some code repetition.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-09 15:04:05 -04:00
Roberto Rosario
6832450221 Generic forms: Don't raise, display exceptions
Update the generic single object create and edit views to display
critical exceptions instead of just raising them and stoping
execution. The should allow removing duplicated exception to message
code in subclasses of these views.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-09 14:59:59 -04:00
Roberto Rosario
e699e39c37 Celery: Remove use of autoretry_for and retry_backoff
These features were for Celery 4.2.1, since this version
has a regression, version 4.1.1 is used instead.
The version 4.2.1 features are removed also and implemented
in code.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-09 01:31:56 -04:00
Roberto Rosario
55e9b2263c Celery: Update Celery to version 4.1.1
Upgrade Celery version used from 3.1.26 to 4.1.1. The following
settings have been renamed: CELERY_ALWAYS_EAGER to
CELERY_TASK_ALWAYS_EAGER, BROKER_URL to CELERY_BROKER_URL.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-08 22:49:15 -04:00
Roberto Rosario
034e0668f4 Caching: Remove caching migration from common app
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-08 17:07:24 -04:00
Roberto Rosario
da6d7cbc0c Converter: Sort ConverterBase methods
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-08 16:34:34 -04:00
Roberto Rosario
2f3d640799 Convert: Move initalization of Pillow
Pillow was being initialized on import. This commit
moves the initialization to the __init__ method of the
Python backend subclass.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-08 16:33:35 -04:00
Roberto Rosario
fad2ae3683 Migrations: Squash migrations
Squash together the following migrations:

  - Common: 0010 to 0011
  - Documents: 0029 to 0037
  - Documents: 0042 to 0043
  - Tags: 0001 to 0008

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-08 03:02:04 -04:00
Roberto Rosario
aaea84b386 Caching: Turn the new caching into its own app
Extract the new smart file caching code from the common app
and convert it into its own new app called file_caching.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-08 01:38:59 -04:00
Roberto Rosario
0c7f9f50af Tests: Mute database convertion tests output
Mute the root mixin to mute all the database conversion tests.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 21:16:07 -04:00
Roberto Rosario
f744eb8871 Documents: Split document.models module
Split the .model module from the document app into separate
modules by purpose. The new modules containing the documents
app models are: document_models.py, document_page_models.py,
document_type_models.py, and document_version_models.py.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 21:11:34 -04:00
Roberto Rosario
3ae991c9cd Style: Minor PEP8 code cleanups
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 20:24:18 -04:00
Roberto Rosario
60233e0b89 Merge branch 'features/explicit_app_paths' into versions/next
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 20:17:45 -04:00
Roberto Rosario
50e72fdb4e Style: Move document model functions
Move the document UUID and document hash functions
to the documents.utils module.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 18:08:47 -04:00
Roberto Rosario
255b1c75ea Style: Prepend "operation_" to data migrations
Prepend "operation_" to the data migration functions
for clear purpose. Add keyword arguments to the RunPython
migration opration.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 17:28:22 -04:00
Roberto Rosario
ab1482152e Documents, Mailer: Sort imports, minor style fix
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 17:20:00 -04:00
Roberto Rosario
adab93fad6 Development: Add a default isort configuration
Add a config file for isort that most closely approaches Mayan's
best practices.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 16:09:14 -04:00
Roberto Rosario
99a1d143ee Tests: Silence expected debug output during tests
Several tests cause errors on purpose to test behaviors.
This commit mute these tests setting their debug level
to critical or by using a context manager to null their
stdout descriptor.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 16:06:53 -04:00
Roberto Rosario
28f387cf6c Metadata: Use generator to prepare lookup choices
Change the use of the list/zip combinarion to generate
the full list of metadata lookup choices to a generator.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 03:34:02 -04:00
Roberto Rosario
8039dfa30a Autoadmin: Incorporate the autoadmin app
Incorporate the external django-autoadmin app as a core app
and convert it into a Mayan app. This change adds the new
settings: "COMMON_AUTOADMIN_EMAIL", "AUTOADMIN_PASSWORD", and
"AUTOADMIN_USERNAME".

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-07 03:09:59 -04:00
Roberto Rosario
28a1ecb685 Events: Display current user events
Add a new view to display the events of the current user.
The link to this view is added to the user menu in the main menu
bar.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 22:01:41 -04:00
Roberto Rosario
3ab41e6b63 Events: Update subscription icon
Update the subscription icon to match other instances.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 22:00:28 -04:00
Roberto Rosario
46cd7353dc Multiple apps: Sort imports and minor style fixes
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 16:38:24 -04:00
Roberto Rosario
4d84b5f28f Common app: Move HOME_VIEW setting
The HOME_VIEW setting is not a Django setting but a setting from the
common app. Move the HOME_VIEW to the COMMON namespace and rename it
to COMMON_HOME_VIEW.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 15:58:19 -04:00
Roberto Rosario
0d9bda0ccf Merge branch 'features/explicit_app_paths' of gitlab.com:mayan-edms/mayan-edms into features/explicit_app_paths 2018-12-06 05:09:54 -04:00
Roberto Rosario
dc255da362 Multiple apps: Default binary path by platform
Use Python's platform library to detect the operating system and
use different default paths for the binary dependencies.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 05:08:35 -04:00
Roberto Rosario
ef126d56b2 Documents: Allow version upload task stubs access
Update which Document model manager the new version upload task
uses to fetch the document. Changing to the passthrough manager
allows the task to access document stubs which is the expected
behavior as new document with no versions are considered stubs.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 03:04:53 -04:00
Roberto Rosario
d6435b7735 Django GPG: 3rd party app compatibility update
Update the django_gpg app to work with the latest version of the
python-gnupg package (0.4.3).

The python-gnupg now returns a more clear error message to
differentiate between a bad passphrase and a missing passphrase.
This improments allows the django_gpg to simplify its error
message parsing and remove the literals:
"ERROR_MSG_NEED_PASSPHRASE" and "ERROR_MSG_GOOD_PASSPHRASE".

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 02:58:40 -04:00
Roberto Rosario
d1a4cb875b Document signatures: Rename settings
Rename the setting "SIGNATURES_STORAGE_BACKEND" to
"DOCUMENT_SIGNATURES_STORAGE_BACKEND" and the setting
"SIGNATURES_STORAGE_BACKEND_ARGUMENTS" to
"DOCUMENT_SIGNATURES_STORAGE_BACKEND_ARGUMENTS". This is
to differentiate these from the settings of the django_gpg
app.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 02:37:25 -04:00
Roberto Rosario
8d8ec59e03 Django GPG: Setting options changes
Remove the SIGNATURES_GPG_HOME settings. The GPG keys are no longer
stored in disk but in the database itself making this setting obsolete.
This changed happened several versions ago and this removal doesn't affect
any code path.

Add two new settings to the app: SIGNATURES_GPG_BACKEND and
SIGNATURES_GPG_BACKEND_ARGUMENTS. These settings allow changing the
GPG backend that the app will use.

Remove the settings SIGNATURES_GPG_PATH. The path to the GPG binary
is now passed via the SIGNATURES_GPG_BACKEND_ARGUMENTS.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 02:34:15 -04:00
Roberto Rosario
55a30379bd Various apps: Sort imports
Run imports as per Mayan EDMS best practices.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-06 01:57:24 -04:00
Roberto Rosario
8740707d1d Documentation: Minor edit in the releases text
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-06 00:48:05 -04:00
Roberto Rosario
27c04ed9be Converter: Remove base64 image support
The get_page method had support to return the image in
base64 format. This feature is no longer used by any
other app.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-05 14:47:12 -04:00
Roberto Rosario
fd4c2e7f00 Converter: Remove base64 image support
The get_page method had support to return the image in
base64 format. This feature is no longer used by any
other app.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2018-12-05 14:45:20 -04:00
Roberto Rosario
685d9b6d3e Converter: Replace deprecated string_concat
Change the use of string_concat with format_lazy.
string_concat is deprecated in Django 1.11 and removed in
Django 2.1.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 04:48:31 -04:00
Roberto Rosario
c430d471e6 Setting: Improve type checking
Update the type checking from using Django's six port
to use direct list and tuple instance checking.

Needed for Python 3.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 04:43:47 -04:00
Roberto Rosario
8aeb7f01fe Converter: Fix failing tests
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 04:43:20 -04:00
Roberto Rosario
d1945b6190 OCR: Update app to use document image cache
Update the OCR app to use the document image cache instead
of trying to read the image file directly from
the document storage.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 04:35:12 -04:00
Roberto Rosario
c466c44dfb Search: Fix failing tests
The SearchModel class no longer returns an elapsed time
value. Update the tests and views that expect this removed
variable.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 03:19:27 -04:00
Roberto Rosario
55cd928069 Documents: Add default filtering of stubs
Add filter(is_stub) to the default Document model manager.

Now only the Passthrough manager can access document stubs.

Remove the explicit filtering of stubs from code that obtains
the queryset from the default document manager.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 03:05:39 -04:00
Roberto Rosario
f6a675c9db Documents: Decrease thumbnail fadein duration
Reduce the document thumbnail fadein animation length
to speed up display of resolved thumbnails.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:49:29 -04:00
Roberto Rosario
2d4a710999 Document: Rename DocumentPageResult model
Rename the DocumentPageResult to DocumentPageSearchResult for
clarity of purpose.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:48:34 -04:00
Roberto Rosario
f9dfc00b30 Project: Fix partial references to apps
All apps references now need to be prepended with mayan.apps.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:19:36 -04:00
Roberto Rosario
2379f6963f Common: Add colorized log formatter
New log formatter that color the output depending on the log
level of the message. The default palette handles: INFO,
SUCCESS, ERROR, DEBUG and CRITICAL.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:12:32 -04:00
Roberto Rosario
59fbbd82e2 Common: Add colorized log formatter
New log formatter that color the output depending on the log
level of the message. The default palette handles: INFO,
SUCCESS, ERROR, DEBUG and CRITICAL.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:07:34 -04:00
Roberto Rosario
8e69178e07 Project: 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 app name claves 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.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-05 02:04:20 -04:00
Roberto Rosario
146ebb7032 Documents: Update cache size on setting update
Add a setting callback to update the size of the document
cache when the DOCUMENTS_CACHE_MAXIMUM_SIZE setting is
updated.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 15:48:44 -04:00
Roberto Rosario
1515c0170f Cache: Prune the cache on property updates
Execute the prune method of a cache when its properties
are changed. This ensures the size of the cache matches
the maximum size set when last saved.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 15:46:05 -04:00
Roberto Rosario
984a1903ce Settings: Add support for post edit callbacks
Settings can be provided with a function via the new
"post_edit_function" argument. This function will be called
when the setting's value is updated. The function will only
receive one argument: The instance of the setting being
changed.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 15:44:08 -04:00
Roberto Rosario
f4e0e06c66 Documents: Remove old image caching model
With the creation of the new general use file Cache system
the old DocumentPageCachedImage model and manager are no longer
needed. This commit removed the model and the manager, and add
a migration to remove any data in the model before removing it
from the database.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 15:21:57 -04:00
Roberto Rosario
66b04296f5 Documentation: Update release notes and changelog
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 00:11:58 -04:00
Roberto Rosario
d6394c5e3b List template: Add missing closing tag
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 00:05:28 -04:00
Roberto Rosario
396f9f6fca Search: Refactor classes
Add additional classes to split existing classes that are too complex.

Remove search timming.

Add query explainer.

Move literals to their own module.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 00:04:41 -04:00
Roberto Rosario
1d1b4f5f5f Documents: Organize methods per best practices
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-04 00:00:42 -04:00
Roberto Rosario
89d3fb9922 Cache: Beta stage
Add retry to race condition in intermediate image generation.

Remove DB index from cache file size field.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-03 04:26:00 -04:00
Roberto Rosario
77fc9b5831 Caching: MVP stage
Working implementation with three levels of organization per cache.
Cache -> Partition -> File.

Current non blocking lock for the intermediate file generation needs
to be replaced to a blocking lock.

Needs tests.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-12-02 05:00:35 -04:00
Roberto Rosario
a414b8df92 Caching: Initial experitmental cache model
signed-off-by: Roberto Rosario <rosarior@t60.lan>
2018-11-30 19:48:26 -04:00
Roberto Rosario
f25174bd15 Configuration: Sane empty result
The settings/base.py file is expeting a dictionary of configuration
options. If there is no config.yml, return and empty dictionary
instead of a None result.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-30 14:55:00 -04:00
Roberto Rosario
7a4d230195 Appearance: Don't hide icons on small screens
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-30 04:36:11 -04:00
Roberto Rosario
d210f05aa6 Appearance: Homogenize the no-result template look
Make sure the no-result template looks the same when included from
all the parent templates.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-30 04:35:54 -04:00
Roberto Rosario
8bf484051e Documents: Delay error handler
Add a delay to the image error handler attachment code to avoid
triggering a false image error event. This is cause when the error
handler is attached before the image is assigned a src attribute.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-30 04:31:59 -04:00
Roberto Rosario
ab045c499c Indexing: Update the default index
Update the default date-based index for the new Jinja2
template language.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-30 04:30:38 -04:00
Roberto Rosario
abfc8b0c09 Navigation: Add list facet menu
Add the new list facet navigation menu. Used to provide facets
to the list subtemplate. The list facet allows separating the object
action links from the object navigation links. The list facet
links are displayed before the list object links on the list
subtemplate. In the object views, the list facet menu behaves
exactly the same as the form facet menu.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-29 04:17:53 -04:00
Roberto Rosario
52bbf62e26 Documents: PEP8 and code style cleanups
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-29 02:11:26 -04:00
Roberto Rosario
e0d900d952 Workflows: Refactor workflow preview generation
Refactor the workflow preview generation to work as a
background task API service. Solves GitLab issue #532.

The image generation runs as an out of process task
ensuring that the HTTP request is never compromised.

A new task queue named "document_states_fast" was created.
The settings WORKFLOWS_IMAGE_CACHE_TIME,
WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND,
WORKFLOWS_IMAGE_CACHE_STORAGE_BACKEND_ARGUMENTS we added.

Images generated are stored by default under /mayan/media/workflows.

The Dockerfile and deployment instructions are updated
to include the new queue.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-29 02:10:31 -04:00
Roberto Rosario
cfe1934b9b Appearance: Fix form CSS media rendering
Fix the way the form CSS contained in the media attribute
is rendered. This is now an interator and not a single value.
Replace the current method with a for loop.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-29 01:12:36 -04:00
Roberto Rosario
ef3453b48c Documentation: Fix error in example setting
The example MAYAN_DATABASES setting was missing the 'default'
database key.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 15:15:39 -04:00
Roberto Rosario
2bd649ab52 Documentation: Add install troubleshooting
Add section to outline common pitfalls when installing.
Reference GitLab issue #487.

Update installation instructons to use the setting
MAYAN_DATABASES instead of the old MAYAN_DATABASE_*.

Unify the installation instructions by converting the
chapters into partials that are now included in the
topic file.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 05:09:49 -04:00
Roberto Rosario
13524f5ce0 Documentation: Update watch folder description
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 04:14:33 -04:00
Roberto Rosario
c6104e0080 Mirroring: Increase cache timeouts
Increase the default value of the index mirroring cache timeout
from 10 seconds to 5 minutes. Since version 3.1.5, mirror
cache invalidation is tied to index updates. This makes the
timeout less relevant. The purpose of the cache timeout is
now avoid runaway memory usage.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 04:13:48 -04:00
Roberto Rosario
f9a0fb2e79 Watch folders: Add support for subfolders
Add support for subfolder scanning to watch folders. Closes
GitLab issue #498.

This commit adds a new field to watch folders called
"include_subdirectories".

The directory walk was also updated to use pathlib2.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 04:00:15 -04:00
Roberto Rosario
d973a20c30 Mailing: Add support for a from field
Add support to the mailing profiles for specifying a "from"
address. Closes GitLab issue #522.

This commit adds a new backend class property "class_fields"
which differs from the normal "fields" property. The "class_fields"
property specifies which of the backend fields will be used to
initialize a backend's driver class. This is to avoid passing
fields that the driver doesn't expect and getting an error.

When sending emails, the "send" method will attempt to get
a "from" key from the backend data and use that when sending
emails. If no "from" key is found a None is passes. Django's
behavior in this situation dictates that the "from" value will
then be taken from the DEFAULT_FROM_EMAIL setting.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 02:59:12 -04:00
Roberto Rosario
b8b10592c7 Mailing: Display a message when testing
A success or failure message will be now displayed when
testing a mailing profile.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 02:57:23 -04:00
Roberto Rosario
96ee283226 Documentation: Improve app chapter formatting
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 00:36:42 -04:00
Roberto Rosario
0188737e74 Settings: Delay initialization of setting
Delay the usage and initialization of Django settings
by removing their reference from the "save_configuration"
method declaration.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 00:35:05 -04:00
Roberto Rosario
46bd5b0a17 Documentation: Improve versioning section
Fix formatting of the versioning section
and turn the list of version examples into a table.
Add further examples for minor and micro releases.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-28 00:33:42 -04:00
Roberto Rosario
10ccab662f Password validations: Add two new validators
Add two new custom password validators. One ensures
passwords have a minimum number of uppercase letters and the
other ensures passwords have a minimum amount of numbers.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 20:07:18 -04:00
Roberto Rosario
77dc53f244 Documentation: Add mention to DEFAULT_FROM_EMAIL
Add mention and example of this new settings option
that is now available.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 20:05:27 -04:00
Roberto Rosario
21d7ec2428 Documentation: Cleanup and code documentation
Cleanup the Sphinx configuration file to comply with PEP8.

Enable the viewcode and autodoc extensions to start adding
code snippets in code.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 20:04:50 -04:00
Roberto Rosario
2ca38c20b0 Tests: Fix failing tests
Fix failing tests in the OCR and parsing apps.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 17:17:07 -04:00
Roberto Rosario
67e79d0e19 OCR, Parsing: Revert iterator stop
Revert how the OCR and document parsing generators end
their iteration. Originally they issue an empty return,
then a blank yield was added. This commit reverts the
blank yield and restores the original 'return' behavior.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 17:15:38 -04:00
Roberto Rosario
141d79afa6 Documentation: Add initial 3.2 release notes file
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 05:31:18 -04:00
Roberto Rosario
e9411514c7 PEP8: Code cleanup
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 05:28:55 -04:00
Roberto Rosario
2f70a57f18 Documentation: Update OCR and parsing indexing examples
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 05:27:01 -04:00
Roberto Rosario
5a626861ae Parsing: Add the 'content' attribute
Add the 'content' attribute to documents to allow access
to a document's parsed content for indexing and other purposes.

Fixes the document parsing indexing failing test.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 05:24:55 -04:00
Roberto Rosario
aaf9f7a8be OCR: Add 'ocr_content' attribute
Add the 'ocr_content' attribute to documents to allow access
to a document's OCR content for indexing and other purposes.

Fixes the OCR indexing failing test.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 05:20:31 -04:00
Roberto Rosario
0f5625a356 Tags: Update test indexing template for Jinja2
Jinja2 doesn't support the {% empty %} node, instead it uses
the {% else %} node for the same purpose.

Fixed the tag indexing failing test.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 05:16:18 -04:00
Roberto Rosario
cb408c768d PEP8: Code cleanups
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 04:26:56 -04:00
Roberto Rosario
03d51fe8e1 PEP8: Code cleanups
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 04:19:35 -04:00
Roberto Rosario
3394f97b25 Settings: Improve lazy values expression
Settings with lazy values are now more carefully checked
and converteed before serializing them.

Previously only the entire value was checked to see if it
was a promise. Now the value is checked to see if it is a
list or tuple and its members checked to see if they are
promises.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 04:17:59 -04:00
Roberto Rosario
7c6d466ab1 Settings: Expose new Django and Celery settings
Expose new Django settings via the UI: AUTH_PASSWORD_VALIDATORS,
DEBUG, DEFAULT_FROM_EMAIL, INTERNAL_IPS, LANGUAGES,
LANGUAGE_CODE, STATIC_URL, STATICFILES_STORAGE,
TIME_ZONE, WSGI_APPLICATION.

Expose a new Celery setting via the UI: CELERY_ALWAYS_EAGER.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 04:15:52 -04:00
Roberto Rosario
ba4858e77f Initial settings: Refactor setting bootstrapping
Refactor the initial environment settings and configuration file loading
fixing some issues loading Django settings.

Consolidate all database settings into a new single setting
called "DATABASES". This mirrors Django database setting
structure. This changes makes it possible to use configure
multiple databases and database routers from the environment
variables or configuration file.

Remove usage of django-environ. Only a small set of the
features provided by django-environ were being used.
Variable typecasting is now only YAML. YAML parsing
is implemented in code.

Previously the initial setting code added all settings
it found into the global symbol table. Now the settings
found are matched to a explicit list of allowed settings.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-27 04:06:21 -04:00
Roberto Rosario
544edea54a Mailer: Code cleanup
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 17:51:08 -04:00
Roberto Rosario
2738e3facf Converter: Move literal to the literls.py module
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 17:50:31 -04:00
Roberto Rosario
51f15a3131 Settings: Update defaults formats
Update the default values of the settings which pass
arguments to backends to be valid Python values and not
YAML strings.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 17:45:43 -04:00
Roberto Rosario
5c1eb59a1a Converter: Rename setting
Change the CONVERTER_GRAPHICS_BACKEND_CONFIG settings
to CONVERTER_GRAPHICS_BACKEND_ARGUMENTS for uniformity
and to convey it real purpose which is initializing a backend.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 17:39:48 -04:00
Roberto Rosario
f5f6470697 Settings: Make namespace a navigation link
Currently the link to navigate back to the settings
namespace is registered as an action when it is a link.
This commits changes it to a link in the facet menu.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 17:29:56 -04:00
Roberto Rosario
d5224d93a7 Settings: Remove support for quoted settings
Instead of passing strings as arguments to backends, all settings must
be formatted according to YAML specifications. This is to remove the
need to add separate YAML parsing to each backend argument in each
app that needs it. Argument passing to backends is not fully
uniform.

Users need to update their config files.
  Example:

    DOCUMENTS_STORAGE_BACKEND_ARGUMENTS: '{location: /home/rosarior/development/mayan-edms/mayan/media/document_storage}'

  must be changed to:

    DOCUMENTS_STORAGE_BACKEND_ARGUMENTS:
      location: /home/rosarior/development/mayan-edms/mayan/media/document_storage

  Example 2:

    CONVERTER_GRAPHICS_BACKEND_CONFIG: '        {            libreoffice_path: /usr/bin/libreoffice,            pdftoppm_dpi:
    300,            pdftoppm_format: jpeg,            pdftoppm_path: /usr/bin/pdftoppm,            pdfinfo_path:
    /usr/bin/pdfinfo,            pillow_format: JPEG        }    '

  must be changed to:

    CONVERTER_GRAPHICS_BACKEND_CONFIG:
      libreoffice_path: /usr/bin/libreoffice
      pdftoppm_dpi: 300
      pdftoppm_format: jpeg
      pdftoppm_path: /usr/bin/pdftoppm
      pdfinfo_path: /usr/bin/pdfinfo
      pillow_format: JPEG

  Example 3:

    OCR_BACKEND_ARGUMENTS: ''

  must be changed to:

    OCR_BACKEND_ARGUMENTS: {}

  Settings that need to be updated are:

  - COMMON_SHARED_STORAGE_ARGUMENTS
  - CONVERTER_GRAPHICS_BACKEND_CONFIG
  - DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS
  - DOCUMENTS_STORAGE_BACKEND_ARGUMENTS
  - OCR_BACKEND_ARGUMENTS
  - SIGNATURES_STORAGE_BACKEND_ARGUMENTS
  - SOURCES_STAGING_FILE_CACHE_STORAGE_BACKEND_ARGUMENTS

  The following error will appear in the console if a setting is not yet
  updated to this new format::

      TypeError: type object argument after ** must be a mapping, not str

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 17:27:57 -04:00
Roberto Rosario
2047fb7b17 Development: Don't add dev apps blindly
Try to import the development apps before adding them
as installed apps.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-26 16:43:34 -04:00
Roberto Rosario
4b727662ef Settings: Update MIDDLEWARE_CLASSES to MIDDLEWARE
Update mentions of MIDDLEWARE_CLASSES to MIDDLEWARE in the
development and testing settings.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-25 02:15:50 -04:00
Roberto Rosario
6a7cd09bc1 Use Jinja2 as the template engine
Use Jinja2 to render the templates of the indexing,
workflows, smart links, user mailer and metadata apps.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-25 02:13:26 -04:00
Roberto Rosario
42d434f7bb Upload Wizard: Add double click support
Add support to select the document type using a Double Click action
on the form.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-25 01:34:44 -04:00
Roberto Rosario
e72b4e82a3 Forms: Add support for form hotkeys
Adds JavaScript support to monitor keypresses or mouse events
of forms with the classes .form-hotkey-enter or form-hotkey-double-click,
and trigger the click event of the button with the CSS class
.btn-hotkey-default.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-25 01:32:22 -04:00
Roberto Rosario
8e896a54f9 Middleware: Modernize middleware classes
Make the custom middleware provided by Mayan to use the
MiddlewareMixin provide by Django. This make the middleware
classes behave like classes or callables. This change ensures
compatibility with Django 2.x.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-25 00:59:39 -04:00
Roberto Rosario
2d5a646940 URLs: Remove development URLs from main URL file
Move the development URL definitions for Rosetta and Debug toolbar
to a separate URL file. Convert the single urls.py to a module to
allow multiple URL files to be used.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-25 00:32:17 -04:00
Roberto Rosario
b3c0b622b8 App: Remove admindocs
Remove admindocs which is not used in production.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-24 22:59:16 -04:00
Roberto Rosario
b04b205fb6 Add docstrings for almost all models
Also adds docstring to some managers and model methods.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-24 22:56:35 -04:00
Roberto Rosario
8c98679687 Dependencies: Remove django-suit from apps
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2018-11-22 04:59:10 -04:00
Roberto Rosario
6d39f3b716 Documents: Add missing import
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 22:02:33 -04:00
Roberto Rosario
21a6ed4756 Documents: Add document page icons
Add icons to the document page image and document page reset
views.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 21:58:10 -04:00
Roberto Rosario
3b247bfb5f Documents: Document task transformations
Add support to pass serialized transformation lists to the
document page generation task.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 21:51:57 -04:00
Roberto Rosario
823083a76c Converter: Add transformation serialization
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 21:51:32 -04:00
Roberto Rosario
4ee6add201 Documents: Document image API transformations
Add transformations support to the document image API.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 21:43:49 -04:00
Roberto Rosario
894a25ccce Appearance: Allow subclassing the TextArea widget
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 18:55:47 -04:00
Roberto Rosario
ecdc4a9a51 Appearance: Remove unused form_empty_label flag
This is superseded by the empty results template.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 18:48:32 -04:00
Roberto Rosario
2ae56d2cf4 Dependencies: Update Python dependencies version.
Remove django suit as a dependency.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 18:41:02 -04:00
Roberto Rosario
810558659d Documents: Add invalid document server template
Invalid document template is now served or included from
a specific template file. Documents with invalid
API image URLs now return None instead of the template code
specific '#'.  The new template is called invalid_document.html.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 18:23:43 -04:00
Roberto Rosario
957cf64fe5 Pagination: Add custom pure pagination subclasses
Add Paginator and Page subclasses that supports custom page
querystring keys.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 18:04:42 -04:00
Roberto Rosario
e8c70cbd08 Pagination: Fix unintented pagination AJAX reload
Add CSS class to disable reload when clicking on pagination active page
link.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2018-11-16 17:59:28 -04:00
4173 changed files with 50784 additions and 118868 deletions

6
.isort.cfg Normal file
View 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

View File

@@ -19,6 +19,12 @@ source_lang = en
source_file = mayan/apps/authentication/locale/en/LC_MESSAGES/django.po source_file = mayan/apps/authentication/locale/en/LC_MESSAGES/django.po
type = 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] [mayan-edms.cabinets-2-0]
file_filter = mayan/apps/cabinets/locale/<lang>/LC_MESSAGES/django.po file_filter = mayan/apps/cabinets/locale/<lang>/LC_MESSAGES/django.po
source_lang = en source_lang = en
@@ -43,6 +49,12 @@ source_lang = en
source_file = mayan/apps/converter/locale/en/LC_MESSAGES/django.po source_file = mayan/apps/converter/locale/en/LC_MESSAGES/django.po
type = 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] [mayan-edms.django_gpg-2-0]
file_filter = mayan/apps/django_gpg/locale/<lang>/LC_MESSAGES/django.po file_filter = mayan/apps/django_gpg/locale/<lang>/LC_MESSAGES/django.po
source_lang = en source_lang = en
@@ -97,6 +109,12 @@ source_lang = en
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po
type = 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] [mayan-edms.linking-2-0]
file_filter = mayan/apps/linking/locale/<lang>/LC_MESSAGES/django.po file_filter = mayan/apps/linking/locale/<lang>/LC_MESSAGES/django.po
source_lang = en source_lang = en

File diff suppressed because it is too large Load Diff

View File

@@ -62,15 +62,16 @@ clean-pyc:
find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} + find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing # Testing
test: test:
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations ./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
test-all: 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: test-launch-postgres:
@docker rm -f test-postgres || true @docker rm -f test-postgres || true
@@ -283,7 +284,7 @@ test-with-docker-frontend:
./manage.py runserver --settings=mayan.settings.staging.docker ./manage.py runserver --settings=mayan.settings.staging.docker
test-with-docker-worker: 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-mysql-on:
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql

View File

@@ -18,7 +18,7 @@ sudo apt-get -qq update
sudo apt-get -y upgrade sudo apt-get -y upgrade
echo -e "\n -> Installing core binaries \n" 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" echo -e "\n -> Cloning development branch of repository \n"
git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY

View File

@@ -1445,15 +1445,15 @@ sudo -u mayan \
dialog --infobox "Preparing static files" 3 70 dialog --infobox "Preparing static files" 3 70
sudo -u mayan \ sudo -u mayan \
MAYAN_MEDIA_ROOT=$MAYAN_MEDIA_ROOT \ 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 # Create supervisor file for gunicorn (frontend), 3 background workers, and the scheduler for periodic tasks
cat > /etc/supervisor/conf.d/mayan.conf <<EOF cat > /etc/supervisor/conf.d/mayan.conf <<EOF
[supervisord] [supervisord]
environment= environment=
MAYAN_ALLOWED_HOSTS="*", # Allow access to other network hosts other than localhost 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_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, PYTHONPATH=${MAYAN_INSTALLATION_FOLDER}/lib/python2.7/site-packages:$MAYAN_MEDIA_ROOT,
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql, MAYAN_DATABASE_ENGINE=django.db.backends.postgresql,
MAYAN_DATABASE_HOST=127.0.0.1, MAYAN_DATABASE_HOST=127.0.0.1,
@@ -1473,7 +1473,7 @@ user = mayan
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -1484,7 +1484,7 @@ user = mayan
[program:mayan-worker-medium] [program:mayan-worker-medium]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -1495,7 +1495,7 @@ user = mayan
[program:mayan-worker-slow] [program:mayan-worker-slow]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998

View File

@@ -20,7 +20,7 @@ apt-get -qq update
apt-get -y upgrade apt-get -y upgrade
echo -e "\n -> Installing core binaries \n" 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" echo -e "\n -> Setting up virtualenv \n"
rm -f ${INSTALLATION_DIRECTORY} 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" 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 cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF
[program:mayan-worker] [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} directory = ${INSTALLATION_DIRECTORY}
user = www-data user = www-data
stdout_logfile = /var/log/mayan/worker-stdout.log stdout_logfile = /var/log/mayan/worker-stdout.log
@@ -161,7 +161,7 @@ priority = 998
EOF EOF
echo -e "\n -> Collecting the static files \n" 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" echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R chown www-data:www-data ${INSTALLATION_DIRECTORY} -R

View File

@@ -9,13 +9,14 @@ import sh
APP_LIST = ( APP_LIST = (
'acls', 'appearance', 'authentication', 'cabinets', 'checkouts', 'common', 'acls', 'appearance', 'authentication', 'autoadmin', 'cabinets',
'converter', 'django_gpg', 'document_comments', 'document_indexing', 'checkouts', 'common', 'converter', 'dashboards', 'django_gpg',
'document_parsing', 'document_signatures', 'document_states', 'documents', 'document_comments', 'document_indexing', 'document_parsing',
'dynamic_search', 'events', 'linking', 'lock_manager', 'mayan_statistics', 'document_signatures', 'document_states', 'documents', 'dynamic_search',
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr', 'permissions', 'events', 'file_caching', 'linking', 'lock_manager', 'mayan_statistics',
'rest_api', 'smart_settings', 'sources', 'storage', 'tags', 'task_manager', 'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr',
'user_management' 'permissions', 'rest_api', 'smart_settings', 'sources', 'storage',
'tags', 'task_manager', 'user_management'
) )
LANGUAGE_LIST = ( LANGUAGE_LIST = (

View File

@@ -21,6 +21,7 @@ RUN if [ "${APT_PROXY}" ]; then echo "Acquire::http { Proxy \"http://${APT_PROXY
# Install base Ubuntu libraries # Install base Ubuntu libraries
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
exiftool \
g++ \ g++ \
gcc \ gcc \
ghostscript \ ghostscript \
@@ -131,11 +132,11 @@ COPY --from=BUILDER_IMAGE /code/docker/version .
RUN chown -R mayan:mayan $PROJECT_INSTALL_DIR RUN chown -R mayan:mayan $PROJECT_INSTALL_DIR
# Install build Mayan EDMS # 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 rm *.whl
# Install Python clients for librabbitmq, MySQL, PostgreSQL, REDIS # 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 # Setup supervisor
COPY docker/etc/supervisor/mayan.conf /etc/supervisor/conf.d COPY docker/etc/supervisor/mayan.conf /etc/supervisor/conf.d

View File

@@ -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) 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) 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. 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` ### `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. 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` ### `MAYAN_NGINX_CLIENT_MAX_BODY_SIZE`

View File

@@ -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) 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) 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. 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` ### `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. 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` ### `MAYAN_NGINX_CLIENT_MAX_BODY_SIZE`

View File

@@ -59,7 +59,7 @@ services:
results: results:
condition: service_healthy condition: service_healthy
environment: 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_CELERY_RESULT_BACKEND: redis://results:6379/0
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db MAYAN_DATABASE_HOST: db

View File

@@ -45,7 +45,7 @@ services:
results: results:
condition: service_healthy condition: service_healthy
environment: 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_CELERY_RESULT_BACKEND: redis://results:6379/0
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db MAYAN_DATABASE_HOST: db

View File

@@ -6,12 +6,12 @@ INSTALL_FLAG=/var/lib/mayan/system/SECRET_KEY
CONCURRENCY_ARGUMENT=--concurrency= CONCURRENCY_ARGUMENT=--concurrency=
export DOCKER_ROOT=/opt/mayan-edms 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_DEFAULT_CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/0
export MAYAN_ALLOWED_HOSTS='["*"]' export MAYAN_ALLOWED_HOSTS='["*"]'
export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py 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_CELERY_RESULT_BACKEND=${MAYAN_CELERY_RESULT_BACKEND:-${MAYAN_DEFAULT_CELERY_RESULT_BACKEND}}
export MAYAN_INSTALL_DIR=/opt/mayan-edms export MAYAN_INSTALL_DIR=/opt/mayan-edms
export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/ export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
@@ -55,13 +55,13 @@ chown mayan:mayan /var/lib/mayan -R
initialize() { initialize() {
echo "mayan: initialize()" echo "mayan: initialize()"
su mayan -c "${MAYAN_BIN} initialsetup --force" su mayan -c "${MAYAN_BIN} initialsetup --force"
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear" su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
} }
upgrade() { upgrade() {
echo "mayan: upgrade()" echo "mayan: upgrade()"
su mayan -c "${MAYAN_BIN} performupgrade" su mayan -c "${MAYAN_BIN} performupgrade"
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear" su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
} }
start() { start() {

View File

@@ -12,7 +12,7 @@ user = mayan
[program:redis] [program:redis]
autorestart = false autorestart = false
autostart = true 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 = /dev/fd/2
stderr_logfile_maxbytes = 0 stderr_logfile_maxbytes = 0
stdout_logfile = /dev/fd/1 stdout_logfile = /dev/fd/1
@@ -22,7 +22,7 @@ user = root
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = false autorestart = false
autostart = true 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -37,7 +37,7 @@ user = mayan
[program:mayan-worker-medium] [program:mayan-worker-medium]
autorestart = false autorestart = false
autostart = true 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -52,7 +52,7 @@ user = mayan
[program:mayan-worker-slow] [program:mayan-worker-slow]
autorestart = false autorestart = false
autostart = true 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998

View File

@@ -136,8 +136,8 @@ Views
The module common.generics provides custom generic class based views to be used. 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 The basic views used to create, edit, view and delete objects in Mayan EDMS
are: SingleObjectCreateView, SingleObjectDetailView, SingleObjectEditView, are: ``SingleObjectCreateView``, ``SingleObjectDetailView``,
and SingleObjectListView ``SingleObjectEditView``, and ``SingleObjectListView``.
These views handle aspects relating to view permissions, object permissions, These views handle aspects relating to view permissions, object permissions,
post action redirection and template context generation. post action redirection and template context generation.

View File

@@ -21,10 +21,10 @@ Binary dependencies
If using a Debian_ or Ubuntu_ based Linux distribution, get the executable If using a Debian_ or Ubuntu_ based Linux distribution, get the executable
requirements using:: requirements using::
sudo apt-get install g++ gcc ghostscript gnupg1 graphviz libfuse2 \ sudo apt-get install exiftool g++ gcc ghostscript gnupg1 graphviz \
libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice libtiff-dev \ libfuse2 libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice \
poppler-utils postgresql python-dev python-virtualenv redis-server \ libtiff-dev poppler-utils postgresql python-dev python-virtualenv \
sane-utils supervisor tesseract-ocr zlib1g-dev -y redis-server sane-utils supervisor tesseract-ocr zlib1g-dev -y
Create an user account for the installation: 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 \ sudo -u mayan MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, HOST=127.0.0.1}}' \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \ MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py initialsetup /opt/mayan-edms/bin/mayan-edms.py initialsetup
Collect the static files: Collect the static files:
@@ -83,7 +82,7 @@ Collect the static files:
:: ::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ 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``: 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] [supervisord]
environment= environment=
MAYAN_ALLOWED_HOSTS='["*"]', # Allow access to other network hosts other than localhost 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_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, PYTHONPATH=/opt/mayan-edms/lib/python2.7/site-packages:/opt/mayan-edms/data,
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media, MAYAN_MEDIA_ROOT=/opt/mayan-edms/media,
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql, MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, HOST: 127.0.0.1, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, CONN_MAX_AGE: 60}}',
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,
DJANGO_SETTINGS_MODULE=mayan.settings.production DJANGO_SETTINGS_MODULE=mayan.settings.production
[program:mayan-gunicorn] [program:mayan-gunicorn]
@@ -113,7 +107,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -124,7 +118,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-medium] [program:mayan-worker-medium]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -135,7 +129,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-slow] [program:mayan-worker-slow]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -188,10 +182,11 @@ Binary dependencies
If using a Debian_ or Ubuntu_ based Linux distribution, get the executable If using a Debian_ or Ubuntu_ based Linux distribution, get the executable
requirements using:: requirements using::
sudo apt-get install g++ gcc ghostscript gnupg1 graphviz libfuse2 \ sudo apt-get install exiftool g++ gcc ghostscript gnupg1 graphviz \
libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice libtiff-dev \ libfuse2 libjpeg-dev libmagic1 libpq-dev libpng-dev libreoffice \
poppler-utils postgresql python-dev python-virtualenv rabbitmq-server \ libtiff-dev poppler-utils postgresql python-dev python-virtualenv \
redis-server sane-utils supervisor tesseract-ocr zlib1g-dev -y rabbitmq-server redis-server sane-utils supervisor tesseract-ocr \
zlib1g-dev -y
Create an user account for the installation: 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 \ sudo -u mayan MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, HOST=127.0.0.1}}' \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \ MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py initialsetup /opt/mayan-edms/bin/mayan-edms.py initialsetup
Collect the static files: Collect the static files:
@@ -250,7 +244,7 @@ Collect the static files:
:: ::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \ 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: Create the RabbitMQ user and vhost:
----------------------------------- -----------------------------------
@@ -267,16 +261,11 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[supervisord] [supervisord]
environment= environment=
MAYAN_ALLOWED_HOSTS='["*"]', # Allow access to other network hosts other than localhost 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_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, PYTHONPATH=/opt/mayan-edms/lib/python2.7/site-packages:/opt/mayan-edms/data,
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media, MAYAN_MEDIA_ROOT=/opt/mayan-edms/media,
MAYAN_DATABASE_ENGINE=django.db.backends.postgresql, MAYAN_DATABASES='{default: {ENGINE: django.db.backends.postgresql, HOST: 127.0.0.1, NAME: mayan, PASSWORD: mayanuserpass, USER: mayan, CONN_MAX_AGE: 60}}',
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,
DJANGO_SETTINGS_MODULE=mayan.settings.production DJANGO_SETTINGS_MODULE=mayan.settings.production
[program:mayan-gunicorn] [program:mayan-gunicorn]
@@ -288,7 +277,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-fast] [program:mayan-worker-fast]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -299,7 +288,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-medium] [program:mayan-worker-medium]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -310,7 +299,7 @@ Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
[program:mayan-worker-slow] [program:mayan-worker-slow]
autorestart = true autorestart = true
autostart = 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 killasgroup = true
numprocs = 1 numprocs = 1
priority = 998 priority = 998
@@ -345,6 +334,36 @@ Enable and restart the services [1_]:
systemctl enable supervisor systemctl enable supervisor
systemctl restart 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 [1]: https://bugs.launchpad.net/ubuntu/+source/supervisor/+bug/1594740
.. _Debian: https://www.debian.org/ .. _Debian: https://www.debian.org/

View File

@@ -447,13 +447,28 @@ Version numbering
================= =================
Mayan EDMS uses the Semantic Versioning (http://semver.org/) method to choose 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/) version numbers along with Python's PEP-0440
to format them. (https://www.python.org/dev/peps/pep-0440/) to format them.
X.YaN # Alpha release +----------------+-------------------+-----------------------------------------+
X.YbN # Beta release | Version number | Name | Description |
X.YrcN # Release Candidate +================+===================+=========================================+
X.Y # Final release | 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 Release checklist

View File

@@ -1,118 +1,10 @@
============ ************
Docker image 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 Stopping and starting the container
----------------------------------- ===================================
To stop the container use:: To stop the container use::
@@ -127,9 +19,11 @@ To start the container again::
.. _docker_environment_variables: .. _docker_environment_variables:
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`` ``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 volume. The SQLite database as used by Mayan EDMS is meant only for development
or testing, never use it in production. 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`` ``MAYAN_DATABASE_HOST``
Defaults to `None`. This optional environment variable is used to set the 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: pertinent Django documentation page:
:django-docs:`Settings, PORT <ref/settings/#port>` :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 This optional environment variable determines the broker that Celery will use
to relay task messages between the frontend code and the background workers. 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. 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 variables are specified, the built-in Redis server inside the container will
be disabled. 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. 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 variables are specified, the built-in Redis server inside the container will
be disabled. be disabled.
@@ -215,12 +88,6 @@ be disabled.
Optional. Allows loading an alternate settings file. 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`` ``MAYAN_GUNICORN_WORKERS``
Optional. This environment variable controls the number of frontend 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 Celery worker to launch its default number of child processes (equal to the
number of CPUs detected). number of CPUs detected).
Accessing outside data 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 is to copy the entire database container volume after the container has
been stopped. been stopped.
Restoring from a backup 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 / sudo tar -xvzf backup.tar.gz -C /
Upgrading 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 docker run -d --name mayan-edms --restart=always -p 80:8000 -v /docker-volumes/mayan:/var/lib/mayan mayanedms/mayanedms:latest
Building the image 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 Replace the IP address `172.17.0.1` with the IP address of the computer
running the APT proxy and caching service. running the APT proxy and caching service.
Customizing the image Customizing the image
===================== =====================
Simple method 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 be downloaded from the Python Package Index (https://pypi.python.org) by
default. default.
Using Docker compose Using Docker compose
==================== ====================

View 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>

View File

@@ -86,11 +86,11 @@ Index by OCR content
This example indexes documents in a "quarterly report" level if they have the This example indexes documents in a "quarterly report" level if they have the
fragment “quarterly report” in the OCR text:: 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:: 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 %}

View File

@@ -15,6 +15,7 @@ via the :ref:`configuration file <configuration_file>`.
Example:: Example::
DEFAULT_FROM_EMAIL: '<your administrator email>'
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: '<your smtp ip address or hostname>' EMAIL_HOST: '<your smtp ip address or hostname>'
EMAIL_HOST_PASSWORD: '<your smtp password>' EMAIL_HOST_PASSWORD: '<your smtp password>'

View 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

View File

@@ -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 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 to the Mayan EDMS container so that it uses the RabbitMQ container the
message broker:: 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 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 calculation, these are stored for a while so that whoever requested the

View File

@@ -37,6 +37,7 @@ http://yaml.org/). Here is an example of what the looks like::
DOCUMENT_PARSING_AUTO_PARSING: true DOCUMENT_PARSING_AUTO_PARSING: true
DOCUMENT_PARSING_PDFTOTEXT_PATH: /usr/bin/pdftotext DOCUMENT_PARSING_PDFTOTEXT_PATH: /usr/bin/pdftotext
DEFAUL_FROM_EMAIL: mayan.admin@example.com
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: localhost EMAIL_HOST: localhost
EMAIL_HOST_PASSWORD: '' EMAIL_HOST_PASSWORD: ''

View File

@@ -15,7 +15,7 @@ The current document sources supported are:
- IMAP email - Same as the ``POP3`` email source but for email accounts using - IMAP email - Same as the ``POP3`` email source but for email accounts using
the ``IMAP`` protocol. the ``IMAP`` protocol.
- Watch folder - A filesystem folder that is scanned periodically for files. - 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 - Staging folder - Folder where networked attached scanned can save image
files. The files in these staging folders are scanned and a preview is 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 generated to help the process of upload. Staging folders and Watch folders

View File

@@ -13,7 +13,8 @@ from __future__ import unicode_literals
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os import os
import sys
sys.path.insert(0, os.path.abspath('..')) 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, # 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 # 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. # 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 ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # 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 # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
#extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
#extensions = ["djangodocs", "sphinx.ext.intersphinx"]
extensions = [ 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_antialias = True
blockdiag_html_image_format = "SVG" blockdiag_html_image_format = 'SVG'
blockdiag_latex_image_format = "PDF" blockdiag_latex_image_format = 'PDF'
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@@ -69,20 +71,20 @@ release = version
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
#language = None # language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
#today = '' # today = ''
# Else, today_fmt is used as the format for a strftime call. # 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 # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents. # 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. # If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True add_function_parentheses = True
@@ -99,7 +101,7 @@ show_authors = False
pygments_style = 'sphinx' pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] # modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- 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 # 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 # further. For a list of options available for each theme, see the
# documentation. # documentation.
#html_theme_options = {} # html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory. # 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 # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
#html_title = None # html_title = None
# A shorter title for the navigation bar. Default is the same as html_title. # 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 # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # 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 # 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 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
#html_favicon = None # html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # 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, # 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, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # 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 # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
#html_use_smartypants = True # html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # 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 # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
#html_additional_pages = {} # html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
#html_domain_indices = True # html_domain_indices = True
# If false, no index is generated. # 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. # 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. # 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. # 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. # 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 # 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 # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # 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"). # 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. # Output file base name for HTML help builder.
htmlhelp_basename = 'MayanEDMSdoc' htmlhelp_basename = 'MayanEDMSdoc'
@@ -188,41 +190,42 @@ html_show_sphinx = False
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4'). # The paper size ('letter' or 'a4').
#latex_paper_size = 'letter' # latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt'). # 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 # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ 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 name of an image file (relative to this directory) to place at the top of
# the title page. # the title page.
#latex_logo = None # latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts, # For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters. # not chapters.
#latex_use_parts = False # latex_use_parts = False
# If true, show page references after internal links. # If true, show page references after internal links.
#latex_show_pagerefs = False # latex_show_pagerefs = False
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
#latex_show_urls = False # latex_show_urls = False
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#latex_preamble = '' # latex_preamble = ''
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
#latex_appendices = [] # latex_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
#latex_domain_indices = True # latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output --------------------------------------------

View File

@@ -31,9 +31,9 @@ for Mayan EDMS. Most MERCs will be Feature MERCs.
2. An **Informational** MERC describes a Mayan EDMS design issue, or 2. An **Informational** MERC describes a Mayan EDMS design issue, or
provides general guidelines or information to the Mayan EDMS community, provides general guidelines or information to the Mayan EDMS community,
but does not propose a new feature. Informational MERCs do not but does not propose a new feature. Informational MERCs do not
necessarily represent a community consensus or necessarily represent a community consensus or recommendation, so users
recommendation, so users and implementers are free to ignore and implementers are free to ignore Informational MERCs or follow their
Informational MERCs or follow their advice. advice.
3. A **Process** MERC describes a process surrounding Mayan EDMS, or 3. A **Process** MERC describes a process surrounding Mayan EDMS, or
proposes a change to (or an event in) a process. Process MERCs are proposes a change to (or an event in) a process. Process MERCs are

View File

@@ -1,6 +1,6 @@
===================== ====================
MERC 2: Test writing MERC 2: Test writing
===================== ====================
:MERC: 2 :MERC: 2
:Author: Michael Price :Author: Michael Price

View 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()
),

View 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)

View File

@@ -20,6 +20,8 @@ Accepted
../mercs/0001-merc-process ../mercs/0001-merc-process
../mercs/0002-test-writing ../mercs/0002-test-writing
../mercs/0003-using-javascript-libraries ../mercs/0003-using-javascript-libraries
../mercs/0005-explicit-arguments
../mercs/0006-lower-information-disclose
Draft Draft
----- -----
@@ -49,3 +51,5 @@ Feature
../mercs/0002-test-writing ../mercs/0002-test-writing
../mercs/0003-using-javascript-libraries ../mercs/0003-using-javascript-libraries
../mercs/0005-explicit-arguments
../mercs/0006-lower-information-disclose

View File

@@ -63,5 +63,5 @@ Changes needed:
the Role model's permissions many to many field. the Role model's permissions many to many field.
4. Update the ``AccessControlList`` models roles field to point to the group 4. Update the ``AccessControlList`` models roles field to point to the group
models. 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. ``AccessControlList`` model manager methods.

View File

@@ -9,10 +9,10 @@ Changes
* Improve index mirroring value clean up code to remove the spaces at the * 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 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 * Improve index mirroring cache class to use the hash of the keys
instead of the literal keys. Avoid warning about invalid key 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. report.
* Only render the Template API view for authenticated users. * Only render the Template API view for authenticated users.
Thanks rgarcia for the report. Thanks rgarcia for the report.

View File

@@ -12,10 +12,10 @@ Changes
* Remove duplicate YAML loading of environment variables. * Remove duplicate YAML loading of environment variables.
* Don't load development apps if they are already loaded. * Don't load development apps if they are already loaded.
* Make sure all key used as input for the cache key hash are * Make sure all key used as input for the cache key hash are
bytes and not unicode. GitLab issue #520. Thanks to TheOneValen bytes and not unicode. GitLab issue #520. Thanks to @TheOneValen for
@TheOneValen for the report. the report.
* Ignore document stub from the index mirror. GitLab issue * 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 * Fix for the Docker image INSTALL_FLAG path. Thanks to
Mark Maglana @relaxdiego for the report and to Hamish Farroq @farroq_HAM Mark Maglana @relaxdiego for the report and to Hamish Farroq @farroq_HAM
for the patch. GitLab issue #525. for the patch. GitLab issue #525.

View File

@@ -8,7 +8,8 @@ Changes
------- -------
* Convert the furl instance to text to allow serializing it into * 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 Removals
-------- --------

106
docs/releases/3.2.rst Normal file
View 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/

View File

@@ -20,6 +20,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
3.2
3.1.9 3.1.9
3.1.8 3.1.8
3.1.7 3.1.7

View File

@@ -9,3 +9,4 @@ Administration
.. include:: ../chapters/backups.rst .. include:: ../chapters/backups.rst
.. include:: ../chapters/scaling_up.rst .. include:: ../chapters/scaling_up.rst
.. include:: ../chapters/database_conversion.rst .. include:: ../chapters/database_conversion.rst
.. include:: ../chapters/docker.rst

View File

@@ -8,3 +8,4 @@ Advanced topics
.. include:: ../chapters/metadata.rst .. include:: ../chapters/metadata.rst
.. include:: ../chapters/transformations.rst .. include:: ../chapters/transformations.rst
.. include:: ../chapters/versioning.rst .. include:: ../chapters/versioning.rst
.. include:: ../chapters/password_validation.rst

View File

@@ -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 is set to True, this mode of operation should only be used for development or
testing. For production deployments the management command:: 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. should be used and the resulting static folder served from a webserver.
For more information check the For more information check the

View File

@@ -2,9 +2,11 @@
Installation Installation
############ ############
The easiest way to use Mayan EDMS is by using the official Docker_ image. Mayan EDMS can be install in several way. The two recommended ways are: by
Make sure Docker is properly installed and working before attempting to install using Docker_, and by doing a direct installation.
Mayan EDMS.
The Docker method provides the easiest installation process while the direct
installation provides better performance and customization.
***************************** *****************************
Minimum hardware requirements Minimum hardware requirements
@@ -15,34 +17,9 @@ Minimum hardware requirements
- Unix-like operating system like Linux and OpenBSD. For other operating systems - Unix-like operating system like Linux and OpenBSD. For other operating systems
user container technologies like Docker or virtual machines. user container technologies like Docker or virtual machines.
****************
Docker procedure
****************
Docker is a computer program that performs operating-system-level .. include:: ../chapters/docker_installation.rst
virtualization also known as containerization. It allows independent .. include:: ../chapters/deploying.rst
"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`
.. _Docker: https://www.docker.com/ .. _Docker: https://www.docker.com/

View File

@@ -2,4 +2,4 @@ from __future__ import unicode_literals
from .classes import ModelPermission # NOQA from .classes import ModelPermission # NOQA
default_app_config = 'acls.apps.ACLsApp' default_app_config = 'mayan.apps.acls.apps.ACLsApp'

View File

@@ -1,203 +1,121 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType from rest_framework import status
from django.shortcuts import get_object_or_404 from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import generics from mayan.apps.common.mixins import ContentTypeViewMixin
from mayan.apps.permissions.serializers import (
from .models import AccessControlList PermissionSerializer, RolePermissionAddRemoveSerializer
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import (
AccessControlListPermissionSerializer, AccessControlListSerializer,
WritableAccessControlListPermissionSerializer,
WritableAccessControlListSerializer
) )
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): class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet):
""" content_type_url_kw_args = {
get: Returns a list of all the object's access control lists 'app_label': 'app_label',
post: Create a new access control list for the selected object. 'model': 'model_name'
""" }
def get_content_object(self): external_object_pk_url_kwarg = 'object_id'
content_type = get_object_or_404( lookup_url_kwarg = 'acl_id'
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.
"""
serializer_class = AccessControlListSerializer serializer_class = AccessControlListSerializer
def get_content_object(self): def create(self, request, *args, **kwargs):
if self.request.method == 'GET': serializer = self.get_serializer(data=request.data)
permission_required = permission_acl_view serializer.is_valid(raise_exception=True)
else: serializer.validated_data.update(
permission_required = permission_acl_edit {
'object_id': self.external_object.pk,
content_type = get_object_or_404( 'content_type': self.get_content_type(),
ContentType, app_label=self.kwargs['app_label'], }
model=self.kwargs['model']
) )
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( def get_external_object_permission(self):
content_type.model_class(), pk=self.kwargs['object_pk'] action = getattr(self, 'action', None)
) if action is None:
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:
return None return None
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs) return permission_acl_view
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListPermissionSerializer
else: else:
return WritableAccessControlListPermissionSerializer return permission_acl_edit
def get_serializer_context(self): def get_external_object_queryset(self):
context = super(APIObjectACLPermissionListView, self).get_serializer_context() # Here we get a queryset the object model for which the event
if self.kwargs: # will be accessed.
context.update( return self.get_content_type().get_all_objects_for_this_type()
{
'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_queryset(self): def get_queryset(self):
return self.get_acl().permissions.all() return self.get_external_object().acls.all()
def get_serializer_context(self): @action(
context = super(APIObjectACLPermissionView, self).get_serializer_context() detail=True, lookup_url_kwarg='acl_id', methods=('post',),
if self.kwargs: serializer_class=RolePermissionAddRemoveSerializer,
context.update( url_name='permission-add', url_path='permissions/add'
{ )
'acl': self.get_acl(), 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
)

View File

@@ -2,35 +2,55 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig, menu_object, menu_sidebar from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
from navigation import SourceColumn 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 from .links import link_acl_create, link_acl_delete, link_acl_permissions
class ACLsApp(MayanAppConfig): class ACLsApp(MayanAppConfig):
app_namespace = 'acls'
app_url = 'acls'
has_rest_api = True has_rest_api = True
has_tests = True has_tests = True
name = 'acls' name = 'mayan.apps.acls'
verbose_name = _('ACLs') verbose_name = _('ACLs')
def ready(self): def ready(self):
super(ACLsApp, self).ready() super(ACLsApp, self).ready()
from actstream import registry
AccessControlList = self.get_model('AccessControlList') AccessControlList = self.get_model(model_name='AccessControlList')
SourceColumn( ModelEventType.register(
source=AccessControlList, label=_('Role'), attribute='role' event_types=(event_acl_created, event_acl_edited),
model=AccessControlList
) )
ModelPermission.register_inheritance(
model=AccessControlList, related='content_object',
)
SourceColumn( SourceColumn(
source=AccessControlList, label=_('Permissions'), attribute='role', is_identifier=True, is_sortable=True,
attribute='get_permission_titles' source=AccessControlList
) )
menu_object.bind_links( 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,) sources=(AccessControlList,)
) )
menu_sidebar.bind_links( menu_secondary.bind_links(
links=(link_acl_create,), sources=('acls:acl_list',) links=(link_acl_create,), sources=('acls:acl_list',)
) )
registry.register(AccessControlList)

View File

@@ -1,4 +1,4 @@
from __future__ import unicode_literals, absolute_import from __future__ import absolute_import, unicode_literals
import logging import logging
@@ -8,23 +8,9 @@ logger = logging.getLogger(__name__)
class ModelPermission(object): class ModelPermission(object):
_registry = {}
_proxies = {}
_inheritances = {} _inheritances = {}
_proxies = {}
@classmethod _registry = {}
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))
@classmethod @classmethod
def get_classes(cls, as_content_type=False): def get_classes(cls, as_content_type=False):
@@ -54,31 +40,38 @@ class ModelPermission(object):
app_label='permissions', model_name='StoredPermission' app_label='permissions', model_name='StoredPermission'
) )
permissions = [] permissions = cls.get_for_class(klass=type(instance))
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))
pks = [ 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) return StoredPermission.objects.filter(pk__in=pks)
@classmethod @classmethod
def register_proxy(cls, source, model): def get_inheritances(cls, model):
cls._proxies[model] = source 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 @classmethod
def register_inheritance(cls, model, related): def register_inheritance(cls, model, related):
cls._inheritances[model] = related cls._inheritances.setdefault(model, [])
cls._inheritances[model].append(related)
@classmethod @classmethod
def get_inheritance(cls, model): def register_proxy(cls, source, model):
return cls._inheritances[model] cls._proxies[model] = source

16
mayan/apps/acls/events.py Normal file
View 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
View 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

View File

@@ -1,6 +1,10 @@
from __future__ import absolute_import, unicode_literals 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_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'
)

View File

@@ -3,10 +3,11 @@ from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_lazy as _ 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 .icons import icon_acl_delete, icon_acl_list, icon_acl_new
from .permissions import permission_acl_view, permission_acl_edit from .permissions import permission_acl_edit, permission_acl_view
def get_kwargs_factory(variable_name): def get_kwargs_factory(variable_name):
@@ -20,7 +21,7 @@ def get_kwargs_factory(variable_name):
) )
return { return {
'app_label': '"{}"'.format(content_type.app_label), 'app_label': '"{}"'.format(content_type.app_label),
'model': '"{}"'.format(content_type.model), 'model_name': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(variable_name) 'object_id': '{}.pk'.format(variable_name)
} }
@@ -28,21 +29,21 @@ def get_kwargs_factory(variable_name):
link_acl_delete = Link( link_acl_delete = Link(
args='resolved_object.pk', permissions=(permission_acl_edit,), icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'},
permissions_related='content_object', tags='dangerous', text=_('Delete'), permission=permission_acl_edit, tags='dangerous', text=_('Delete'),
view='acls:acl_delete', view='acls:acl_delete',
) )
link_acl_list = Link( link_acl_list = Link(
icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'), icon_class=icon_acl_list, kwargs=get_kwargs_factory(
permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list' variable_name='resolved_object'
), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list'
) )
link_acl_create = Link( link_acl_create = Link(
icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'), icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_acl_edit,), text=_('New ACL'), permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create'
view='acls:acl_create'
) )
link_acl_permissions = Link( link_acl_permissions = Link(
args='resolved_object.pk', permissions=(permission_acl_edit,), args='resolved_object.pk', icon_class=icon_permission,
permissions_related='content_object', text=_('Permissions'), permission=permission_acl_edit, text=_('Permissions'),
view='acls:acl_permissions', view='acls:acl_permissions',
) )

View File

@@ -1,19 +1,26 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import logging import logging
import operator
import warnings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import CharField, Value as V, Q
from django.utils.translation import ugettext, ugettext_lazy as _ from django.db.models.functions import Concat
from django.http import Http404
from common.utils import return_attrib, return_related from mayan.apps.common.utils import (
from permissions import Permission get_related_field, resolve_attribute, return_related
from permissions.models import StoredPermission )
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 .classes import ModelPermission
from .exceptions import PermissionNotValidForClass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,200 +30,189 @@ class AccessControlListManager(models.Manager):
Implement a 3 tier permission system, involving a permissions, an actor Implement a 3 tier permission system, involving a permissions, an actor
and an object and an object
""" """
def check_access(self, permissions, user, obj, related=None): def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None):
if user.is_superuser or user.is_staff: """
logger.debug( This method does the bulk of the work. It generates filters for the
'Permissions "%s" on "%s" granted to user "%s" as superuser ' AccessControlList model to determine if there are ACL entries for the
'or staff', permissions, obj, user 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: if isinstance(related_field, GenericForeignKey):
return Permission.check_permissions( # Case 3: Generic Foreign Key, multiple ContentTypes + object
requester=user, permissions=permissions # id combinations
) content_type_object_id_queryset = queryset.annotate(
except PermissionDenied: ct_fk_combination=Concat(
try: related_field.ct_field, V('-'), related_field.fk_field,
stored_permissions = [ output_field=CharField()
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
) )
except AttributeError: ).values('ct_fk_combination')
# 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
user_roles = [] acl_filter = self.annotate(
for group in user.groups.all(): ct_fk_combination=Concat(
for role in group.roles.all(): 'content_type', V('-'), 'object_id', output_field=CharField()
if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))): )
logger.debug( ).filter(
'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL', permissions=stored_permission, role__groups__user=user,
permissions, obj, user, role ct_fk_combination__in=content_type_object_id_queryset
) ).values('object_id')
return True
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(): result.append(Q(**{field_lookup: acl_filter}))
logger.debug( else:
'Permissions "%s" on "%s" denied for user "%s"', # Case 2: Related field of a single type, single ContentType,
permissions, obj, user # 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( relation_result.append(reduce(operator.and_, related_field_inherited_acl_queries))
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
permissions, obj, user, user_roles
)
def filter_by_access(self, permission, user, queryset): result.append(reduce(operator.or_, relation_result))
if user.is_superuser or user.is_staff: else:
logger.debug( # Case 1: Original model, single ContentType, multiple object id
'Unfiltered queryset returned to user "%s" as superuser or staff', content_type = ContentType.objects.get_for_model(model=queryset.model)
user field_lookup = 'id__in'
) acl_filter = self.filter(
return queryset content_type=content_type, permissions=stored_permission,
role__groups__user=user
try: ).values('object_id')
Permission.check_permissions( result.append(Q(**{field_lookup: acl_filter}))
requester=user, permissions=(permission,)
)
except PermissionDenied:
user_roles = []
for group in user.groups.all():
for role in group.roles.all():
user_roles.append(role)
# Case 4: Original model, has an inherited related field
try: try:
parent_accessor = ModelPermission.get_inheritance( related_fields = ModelPermission.get_inheritances(
model=queryset.model model=queryset.model
) )
except KeyError: except KeyError:
parent_acl_query = Q() pass
else: else:
instance = queryset.first() relation_result = []
if instance:
parent_object = return_related( for related_field_name in related_fields:
instance=instance, related_field=parent_accessor 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: result.append(reduce(operator.or_, relation_result))
# 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)
return queryset.filter(pk__in=result) return result
else:
parent_acl_query = Q()
# Directly granted access def check_access(self, obj, permission, user, raise_404=False):
content_type = ContentType.objects.get_for_model(queryset.model) warnings.warn(
acl_query = Q(pk__in=self.filter( 'check_access() is deprecated, use restrict_queryset() to '
content_type=content_type, role__in=user_roles, 'produce a queryset from which to .get() the corresponding '
permissions=permission.stored_permission 'object in the local code.', InterfaceWarning
).values_list('object_id', flat=True)) )
logger.debug( queryset = self.restrict_queryset(
'Filtered queryset returned to user "%s" based on roles "%s"', permission=permission, queryset=obj._meta.default_manager.all(),
user, user_roles user=user
) )
return queryset.filter(parent_acl_query | acl_query) if queryset.filter(pk=obj.pk).exists():
return True
else: 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 return queryset
def get_inherited_permissions(self, role, obj):
try: try:
instance = obj.first() related_fields = ModelPermission.get_inheritances(
except AttributeError: model=type(obj)
instance = obj )
else:
if not instance:
return StoredPermission.objects.none()
try:
parent_accessor = ModelPermission.get_inheritance(type(instance))
except KeyError: except KeyError:
return StoredPermission.objects.none() pass
else: else:
try: for related_field_name in related_fields:
parent_object = return_attrib( try:
obj=instance, attrib=parent_accessor parent_object = resolve_attribute(
) obj=obj, attribute=related_field_name
except AttributeError: )
# Parent accessor is not an attribute, try it as a related except AttributeError:
# field. # Parent accessor is not an attribute, try it as a related
parent_object = return_related( # field.
instance=instance, related_field=parent_accessor parent_object = return_related(
) instance=obj, related_field=related_field_name
content_type = ContentType.objects.get_for_model(parent_object) )
try: content_type = ContentType.objects.get_for_model(model=parent_object)
return self.get( try:
role=role, content_type=content_type, queryset = queryset | self.get(
object_id=parent_object.pk content_type=content_type, object_id=parent_object.pk,
).permissions.all() role=role
except self.model.DoesNotExist: ).permissions.all()
return StoredPermission.objects.none() 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__) class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
if permission not in class_permissions: if permission not in class_permissions:
raise PermissionNotValidForClass raise PermissionNotValidForClass
@@ -229,7 +225,44 @@ class AccessControlListManager(models.Manager):
acl.permissions.add(permission.stored_permission) 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) content_type = ContentType.objects.get_for_model(model=obj)
acl, created = self.get_or_create( acl, created = self.get_or_create(
content_type=content_type, object_id=obj.pk, content_type=content_type, object_id=obj.pk,

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models, migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -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 __future__ import unicode_literals
from django.db import migrations from django.db import migrations
@@ -14,6 +12,9 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='accesscontrollist', 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'
},
), ),
] ]

View File

@@ -1,15 +1,18 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import logging import logging
import operator
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType 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.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ 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 from .managers import AccessControlListManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,6 +32,11 @@ class AccessControlList(models.Model):
* Role - Custom role that is being granted a permission. Roles are created * Role - Custom role that is being granted a permission. Roles are created
in the Setup menu. in the Setup menu.
""" """
# Multiple inheritance operator types
OPERATOR_AND = operator.and_
OPERATOR_OR = operator.or_
operator_default = OPERATOR_AND
content_type = models.ForeignKey( content_type = models.ForeignKey(
on_delete=models.CASCADE, related_name='object_content_type', on_delete=models.CASCADE, related_name='object_content_type',
to=ContentType to=ContentType
@@ -57,21 +65,58 @@ class AccessControlList(models.Model):
def __str__(self): def __str__(self):
return _( 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, '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): def get_inherited_permissions(self):
return AccessControlList.objects.get_inherited_permissions( return AccessControlList.objects.get_inherited_permissions(
role=self.role, obj=self.content_object role=self.role, obj=self.content_object
) )
def get_permission_titles(self): def get_permission_titles(self):
"""
Returns the descriptibe labels for the permissions.
"""
result = ', '.join( result = ', '.join(
[force_text(permission) for permission in self.permissions.all()] [force_text(permission) for permission in self.permissions.all()]
) )
return result or _('None') 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
)

View File

@@ -2,13 +2,13 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ 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( permission_acl_edit = namespace.add_permission(
name='acl_edit', label=_('Edit ACLs') label=_('Edit ACLs'), name='acl_edit'
) )
permission_acl_view = namespace.add_permission( permission_acl_view = namespace.add_permission(
name='acl_view', label=_('View ACLs') label=_('View ACLs'), name='acl_view'
) )

View File

@@ -1,204 +1,143 @@
from __future__ import absolute_import, unicode_literals 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 django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from common.serializers import ContentTypeSerializer from mayan.apps.common.serializers import ContentTypeSerializer
from permissions import Permission from mayan.apps.permissions.models import Role
from permissions.models import Role, StoredPermission from mayan.apps.permissions.permissions import permission_role_edit
from permissions.serializers import PermissionSerializer, RoleSerializer 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 from .models import AccessControlList
class AccessControlListSerializer(serializers.ModelSerializer): class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True) 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) 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: class Meta:
external_object_model = Role
external_object_pk_field = 'role_id'
external_object_permission = permission_role_edit
fields = ( 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 model = AccessControlList
read_only_fields = ('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']
)
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',)
def create(self, validated_data): def create(self, validated_data):
for permission in validated_data['permissions']: role = self.get_external_object()
self.context['acl'].permissions.add(permission)
return validated_data['permissions'][0] if role:
validated_data['role'] = role
def validate(self, attrs): return super(AccessControlListSerializer, self).create(
permissions_pk_list = attrs.pop('permission_pk', None) validated_data=validated_data
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']
) )
def get_url(self, instance): def update(self, instance, validated_data):
return reverse( role = self.get_external_object()
'rest_api:accesscontrollist-detail', args=(
instance.content_type.app_label, instance.content_type.model, if role:
instance.object_id, instance.pk validated_data['role'] = role
), request=self.context['request'], format=self.context['format']
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

View 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()

View File

@@ -2,23 +2,20 @@ from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from document_states.tests.test_actions import ActionTestCase from mayan.apps.document_states.tests.test_actions import ActionTestCase
from documents.permissions import permission_document_view from mayan.apps.documents.permissions import permission_document_view
from ..workflow_actions import GrantAccessAction, RevokeAccessAction from ..workflow_actions import GrantAccessAction, RevokeAccessAction
class ACLActionTestCase(ActionTestCase): class ACLActionTestCase(ActionTestCase):
def setUp(self):
super(ACLActionTestCase, self).setUp()
def test_grant_access_action(self): def test_grant_access_action(self):
action = GrantAccessAction( action = GrantAccessAction(
form_data={ form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk, 'object_id': self.document.pk,
'roles': [self.role.pk], 'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.uuid], 'permissions': [permission_document_view.pk],
} }
) )
action.execute(context={'entry_log': self.entry_log}) action.execute(context={'entry_log': self.entry_log})
@@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase):
list(self.document.acls.first().permissions.all()), list(self.document.acls.first().permissions.all()),
[permission_document_view.stored_permission] [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): def test_revoke_access_action(self):
self.grant_access( self.grant_access(
@@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase):
form_data={ form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk, 'object_id': self.document.pk,
'roles': [self.role.pk], 'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.uuid], 'permissions': [permission_document_view.pk],
} }
) )
action.execute(context={'entry_log': self.entry_log}) action.execute(context={'entry_log': self.entry_log})

View File

@@ -1,205 +1,189 @@
from __future__ import absolute_import, unicode_literals 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 rest_framework import status
from documents.permissions import permission_document_view from mayan.apps.rest_api.tests import BaseAPITestCase
from documents.tests import DocumentTestMixin
from permissions.tests.literals import TEST_ROLE_LABEL
from rest_api.tests import BaseAPITestCase
from ..models import AccessControlList 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(ACLTestMixin, BaseAPITestCase):
class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
def setUp(self): def setUp(self):
super(ACLAPITestCase, self).setUp() 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( def _request_object_acl_list_api_view(self):
self.document return self.get(
viewname='rest_api:object-acl-list',
kwargs=self.test_content_object_view_kwargs
) )
def _create_acl(self): def test_object_acl_list_api_view_no_permission(self):
self.acl = AccessControlList.objects.create( response = self._request_object_acl_list_api_view()
content_object=self.document, self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
role=self.role
)
self.acl.permissions.add(permission_document_view.stored_permission) def test_object_acl_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
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
)
)
response = self._request_object_acl_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data['results'][0]['content_type']['app_label'], response.data['results'][0]['content_type']['app_label'],
self.document_content_type.app_label self.test_object_content_type.app_label
) )
self.assertEqual( 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): def _request_acl_delete_api_view(self):
self._create_acl() kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
response = self.delete( return self.delete(
viewname='rest_api:accesscontrollist-detail', viewname='rest_api:object-acl-detail',
args=( kwargs=kwargs
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
) )
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(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): def test_object_acl_delete_api_view_no_permission(self):
self._create_acl() response = self._request_acl_delete_api_view()
response = self.get( self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
viewname='rest_api:accesscontrollist-detail', self.assertTrue(self.test_acl in AccessControlList.objects.all())
args=(
self.document_content_type.app_label, def _request_object_acl_detail_api_view(self):
self.document_content_type.model, kwargs = self.test_content_object_view_kwargs.copy()
self.document.pk, self.acl.pk 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( self.assertEqual(
response.data['content_type']['app_label'], response.data['content_type']['app_label'],
self.document_content_type.app_label self.test_object_content_type.app_label
) )
self.assertEqual( 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): def test_object_acl_detail_api_view_no_permission(self):
self._create_acl() response = self._request_object_acl_detail_api_view()
permission = self.acl.permissions.first() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.delete( def _request_object_acl_permission_list_api_view(self):
viewname='rest_api:accesscontrollist-permission-detail', kwargs = self.test_content_object_view_kwargs.copy()
args=( kwargs['acl_id'] = self.test_acl.pk
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 test_object_acl_permission_detail_view(self): return self.get(
self._create_acl() viewname='rest_api:object-acl-permission-list',
permission = self.acl.permissions.first() kwargs=kwargs
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
)
) )
self.assertEqual( def test_object_acl_permission_list_api_view_with_access(self):
response.data['pk'], permission_document_view.pk self.grant_access(obj=self.test_object, permission=permission_acl_view)
)
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
)
)
response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual( self.assertEqual(
response.data['results'][0]['pk'], response.data['results'][0]['pk'],
permission_document_view.pk self.test_permission.pk
) )
def test_object_acl_permission_list_post_view(self): def test_object_acl_permission_list_api_view_no_permission(self):
self._create_acl() response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.post( def _request_object_acl_permission_remove_api_view(self):
viewname='rest_api:accesscontrollist-permission-list', kwargs = self.test_content_object_view_kwargs.copy()
args=( kwargs['acl_id'] = self.test_acl.pk
self.document_content_type.app_label,
self.document_content_type.model, return self.post(
self.document.pk, self.acl.pk viewname='rest_api:object-acl-permission-remove',
), data={'permission_pk': permission_acl_view.pk} kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_object_acl_permission_remove_api_view_with_access(self):
self.assertQuerysetEqual( self.grant_access(obj=self.test_object, permission=permission_acl_edit)
ordered=False, qs=self.acl.permissions.all(), values=(
repr(permission_document_view.stored_permission), response = self._request_object_acl_permission_remove_api_view()
repr(permission_acl_view.stored_permission) 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): def test_object_acl_permission_add_api_view_with_access(self):
response = self.post( self.test_acl.permissions.clear()
viewname='rest_api:accesscontrollist-list', self.grant_access(obj=self.test_object, permission=permission_acl_edit)
args=(
self.document_content_type.app_label, response = self._request_object_acl_permission_add_api_view()
self.document_content_type.model, self.assertEqual(response.status_code, status.HTTP_200_OK)
self.document.pk self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
), data={'role_pk': self.role.pk}
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.assertEqual(
self.document.acls.first().role, self.role response.data['results'][0]['pk'],
) self.test_permission.pk
self.assertEqual(
self.document.acls.first().content_object, self.document
)
self.assertEqual(
self.document.acls.first().permissions.count(), 0
) )
def test_object_acl_post_with_permissions_added_view(self): def test_object_acl_inherited_permission_list_api_view_no_permission(self):
response = self.post( self.test_acl.permissions.clear()
viewname='rest_api:accesscontrollist-list', self.test_role.grant(permission=self.test_permission)
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
} response = self._request_object_acl_inherited_permission_list_api_view()
) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
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
)

View File

@@ -1,100 +1,84 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from documents.tests import GenericDocumentViewTestCase from mayan.apps.common.tests import GenericViewTestCase
from ..links import ( 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 ..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) class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
self.login_user() 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() context = self.get_test_view()
resolved_link = link_acl_create.resolve(context=context) resolved_link = link_acl_create.resolve(context=context)
self.assertNotEqual(resolved_link, None) 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( 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): def test_object_acl_delete_link(self):
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_edit)
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission) self.add_test_view(test_object=self._test_case_acl)
self.login_user()
self.add_test_view(test_object=acl)
context = self.get_test_view() context = self.get_test_view()
resolved_link = link_acl_delete.resolve(context=context) resolved_link = link_acl_delete.resolve(context=context)
self.assertNotEqual(resolved_link, None) self.assertNotEqual(resolved_link, None)
self.assertEqual( 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): def test_object_acl_edit_link(self):
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_edit)
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission) self.add_test_view(test_object=self._test_case_acl)
self.login_user()
self.add_test_view(test_object=acl)
context = self.get_test_view() context = self.get_test_view()
resolved_link = link_acl_permissions.resolve(context=context) resolved_link = link_acl_permissions.resolve(context=context)
self.assertNotEqual(resolved_link, None) self.assertNotEqual(resolved_link, None)
self.assertEqual( 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): def test_object_acl_list_link(self):
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_view)
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_view.stored_permission) self.add_test_view(test_object=self.test_object)
self.login_user()
self.add_test_view(test_object=self.document)
context = self.get_test_view() context = self.get_test_view()
resolved_link = link_acl_list.resolve(context=context) resolved_link = link_acl_list.resolve(context=context)
self.assertNotEqual(resolved_link, None) 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( 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
)
) )

View File

@@ -1,161 +1,401 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.test import override_settings from django.db import models
from common.tests import BaseTestCase from mayan.apps.common.tests import BaseTestCase
from documents.models import Document, DocumentType from mayan.apps.documents.models import Document, DocumentType
from documents.permissions import permission_document_view from mayan.apps.documents.permissions import permission_document_view
from documents.tests import ( from mayan.apps.documents.tests import (
TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE_LABEL, DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
TEST_DOCUMENT_TYPE_2_LABEL
) )
from ..classes import ModelPermission
from ..models import AccessControlList 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): def setUp(self):
super(PermissionTestCase, self).setUp() super(PermissionTestCase, self).setUp()
self.document_type_1 = DocumentType.objects.create( self.test_document_type_1 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_LABEL 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 label=TEST_DOCUMENT_TYPE_2_LABEL
) )
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: self.test_document_1 = self.upload_document(
self.document_1 = self.document_type_1.new_document( document_type=self.test_document_type_1
file_object=file_object )
) self.test_document_2 = self.upload_document(
document_type=self.test_document_type_1
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: )
self.document_2 = self.document_type_1.new_document( self.test_document_3 = self.upload_document(
file_object=file_object document_type=self.test_document_type_2
) )
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()
def test_check_access_without_permissions(self): def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied): with self.assertRaises(PermissionDenied):
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), obj=self.test_document_1, permission=permission_document_view,
user=self.user, obj=self.document_1 user=self._test_case_user
) )
def test_filtering_without_permissions(self): def test_filtering_without_permissions(self):
self.assertQuerysetEqual( self.assertEqual(
AccessControlList.objects.filter_by_access( AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view,
queryset=Document.objects.all() queryset=Document.objects.all(), user=self._test_case_user,
), [] ).count(), 0
) )
def test_check_access_with_acl(self): def test_check_access_with_acl(self):
acl = AccessControlList.objects.create( 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) acl.permissions.add(permission_document_view.stored_permission)
try: try:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user, obj=self.test_document_1, permission=permission_document_view,
obj=self.document_1 user=self._test_case_user
) )
except PermissionDenied: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_permissions(self): def test_filtering_with_permissions(self):
acl = AccessControlList.objects.create( 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) acl.permissions.add(permission_document_view.stored_permission)
self.assertQuerysetEqual( self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access( AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view,
queryset=Document.objects.all() queryset=Document.objects.all(), user=self._test_case_user
), (repr(self.document_1),) ), (repr(self.test_document_1),)
) )
def test_check_access_with_inherited_acl(self): def test_check_access_with_inherited_acl(self):
acl = AccessControlList.objects.create( 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.permissions.add(permission_document_view.stored_permission)
try: try:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user, obj=self.test_document_1, permission=permission_document_view,
obj=self.document_1 user=self._test_case_user
) )
except PermissionDenied: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
def test_check_access_with_inherited_acl_and_local_acl(self): def test_check_access_with_inherited_acl_and_direct_acl(self):
acl = AccessControlList.objects.create( test_acl_1 = 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) test_acl_1.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create( test_acl_2 = 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) test_acl_2.permissions.add(permission_document_view.stored_permission)
try: try:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user, obj=self.test_document_3, permission=permission_document_view,
obj=self.document_3 user=self._test_case_user
) )
except PermissionDenied: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_inherited_permissions(self): def test_filtering_with_inherited_permissions(self):
acl = AccessControlList.objects.create( 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.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access( result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view, queryset=Document.objects.all(),
queryset=Document.objects.all() user=self._test_case_user
) )
# Since document_1 and document_2 are of document_type_1 # Since document_1 and document_2 are of document_type_1
# they are the only ones that should be returned # they are the only ones that should be returned
self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.document_1 in result) self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.document_2 in result) self.assertTrue(self.test_document_3 not in result)
self.assertTrue(self.document_3 not in result)
def test_filtering_with_inherited_permissions_and_local_acl(self): 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( 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.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create( 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) acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access( result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view, queryset=Document.objects.all(),
queryset=Document.objects.all() user=self._test_case_user,
) )
self.assertTrue(self.document_1 in result) self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.document_2 in result) self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.document_3 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)

View File

@@ -1,191 +1,239 @@
from __future__ import absolute_import, unicode_literals 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 ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class AccessControlListViewTestCase(GenericDocumentViewTestCase):
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
def setUp(self): def setUp(self):
super(AccessControlListViewTestCase, self).setUp() 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 = { self._create_test_permission()
'app_label': content_type.app_label, ModelPermission.register(
'model': content_type.model, model=self.test_object._meta.model, permissions=(
'object_id': self.document.pk self.test_permission,
} )
)
def test_acl_create_view_no_permission(self): self._inject_test_object_content_type()
self.login_user()
response = self.get( self._create_test_acl()
viewname='acls:acl_create', kwargs=self.view_arguments, data={ self.test_acl.permissions.add(self.test_permission.stored_permission)
'role': self.role.pk
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) def test_acl_create_get_view_no_permission(self):
self.assertEqual(AccessControlList.objects.count(), 0) self.test_acl.delete()
def test_acl_create_view_with_permission(self): response = self._request_acl_create_get_view()
self.login_user() self.assertEqual(response.status_code, 404)
self.role.permissions.add( self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
permission_acl_edit.stored_permission
)
response = self.get( def test_acl_create_get_view_with_object_access(self):
viewname='acls:acl_create', kwargs=self.view_arguments, data={ self.test_acl.delete()
'role': self.role.pk self.grant_access(obj=self.test_object, permission=permission_acl_edit)
}, follow=True
)
response = self._request_acl_create_get_view()
self.assertContains( 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): def test_acl_create_view_post_no_permission(self):
self.login_user() self.test_acl.delete()
response = self.post( response = self._request_acl_create_post_view()
viewname='acls:acl_create', kwargs=self.view_arguments, data={ self.assertEqual(response.status_code, 404)
'role': self.role.pk
}
)
self.assertEquals(response.status_code, 403) self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_view_with_post_permission(self): def test_acl_create_view_post_with_access(self):
self.login_user() self.test_acl.delete()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
self.role.permissions.add( response = self._request_acl_create_post_view()
permission_acl_edit.stored_permission self.assertEqual(response.status_code, 302)
)
response = self.post( self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
self.assertContains(response, text='created', status_code=200) def test_acl_create_duplicate_view_with_access(self):
self.assertEqual(AccessControlList.objects.count(), 1)
def test_acl_create_duplicate_view_with_permission(self):
""" """
Test creating a duplicate ACL entry: same object & role Test creating a duplicate ACL entry: same object & role
Result: Should redirect to existing ACL for object + role combination Result: Should redirect to existing ACL for object + role combination
""" """
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_edit)
content_object=self.document, role=self.role
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( # Sorted by role PK
permission_acl_edit.stored_permission 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( self.assertQuerysetEqual(
viewname='acls:acl_create', kwargs=self.view_arguments, data={ qs=AccessControlList.objects.order_by('role__id').values(
'role': self.role.pk 'object_id', 'role',
}, follow=True ), transform=dict, values=expected_results
) )
self.assertContains( def _request_acl_delete_view(self):
response, text='vailable permissions', status_code=200 return self.post(
) viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk}
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
) )
recent_entry = self.document.add_as_recent_document_for_user(self.user) def test_acl_delete_view_no_permission(self):
response = self._request_acl_delete_view()
content_type = ContentType.objects.get_for_model(recent_entry) self.assertNotContains(
response=response, text=force_text(self.test_object),
view_arguments = { status_code=404
'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
) )
self.assertNotContains(response, text='optgroup', status_code=200) self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
self.assertEqual(AccessControlList.objects.count(), 1)
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): 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.assertNotContains(
self.user response=response, text=force_text(self.test_object),
).document status_code=404
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, text=document.label, status_code=403) def test_acl_list_view_with_access(self):
self.assertNotContains(response, text='otal: 1', status_code=403) self.grant_access(obj=self.test_object, permission=permission_acl_view)
def test_acl_list_view_with_permission(self): response = self._request_acl_list_view()
self.login_user()
self.role.permissions.add( self.assertContains(
permission_acl_view.stored_permission response=response, text=force_text(self.test_object),
status_code=200
) )
document = self.document.add_as_recent_document_for_user( def _request_get_acl_permissions_get_view(self):
self.user return self.get(
).document viewname='acls:acl_permissions',
kwargs={'acl_id': self.test_acl.pk}
acl = AccessControlList.objects.create(
content_object=document, role=self.role
) )
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 = { response = self._request_get_acl_permissions_get_view()
'app_label': content_type.app_label, self.assertNotContains(
'model': content_type.model, response=response, text=force_text(self.test_object),
'object_id': document.pk status_code=404
} )
response = self.get( self.assertFalse(
viewname='acls:acl_list', kwargs=view_arguments 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)

View File

@@ -2,45 +2,33 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import ( from .api_views import ObjectACLAPIViewSet
APIObjectACLListView, APIObjectACLPermissionListView,
APIObjectACLPermissionView, APIObjectACLView
)
from .views import ( from .views import (
ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
) )
urlpatterns = [ urlpatterns = [
url( url(
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/create/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model_name>[-\w]+)/(?P<object_id>\d+)/create/$',
ACLCreateView.as_view(), name='acl_create' name='acl_create', view=ACLCreateView.as_view()
), ),
url( url(
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model_name>[-\w]+)/(?P<object_id>\d+)/list/$',
ACLListView.as_view(), name='acl_list' name='acl_list', view=ACLListView.as_view()
), ),
url(r'^(?P<pk>\d+)/delete/$', ACLDeleteView.as_view(), name='acl_delete'),
url( url(
r'^(?P<pk>\d+)/permissions/$', ACLPermissionsView.as_view(), regex=r'^acls/(?P<acl_id>\d+)/delete/$', name='acl_delete',
name='acl_permissions' view=ACLDeleteView.as_view()
),
url(
regex=r'^acls/(?P<acl_id>\d+)/permissions/$', name='acl_permissions',
view=ACLPermissionsView.as_view()
), ),
] ]
api_urls = [ api_router_entries = (
url( {
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$', 'prefix': r'apps/(?P<app_label>[^/.]+)/models/(?P<model_name>[^/.]+)/objects/(?P<object_id>[^/.]+)/acls',
APIObjectACLListView.as_view(), name='accesscontrollist-list' 'viewset': ObjectACLAPIViewSet, 'basename': 'object-acl'
), },
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'
),
]

View File

@@ -1,24 +1,23 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import itertools
import logging 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.template import RequestContext
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.views import ( from mayan.apps.common.mixins import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, ContentTypeViewMixin, ExternalObjectMixin
)
from mayan.apps.common.generics import (
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView SingleObjectListView
) )
from permissions import PermissionNamespace, Permission from mayan.apps.permissions.models import Role
from permissions.models import StoredPermission
from .classes import ModelPermission from .classes import ModelPermission
from .forms import ACLCreateForm
from .icons import icon_acl_list from .icons import icon_acl_list
from .links import link_acl_create from .links import link_acl_create
from .models import AccessControlList from .models import AccessControlList
@@ -27,113 +26,95 @@ from .permissions import permission_acl_edit, permission_acl_view
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ACLCreateView(SingleObjectCreateView): class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView):
fields = ('role',) content_type_url_kw_args = {
model = AccessControlList '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): def get_error_message_duplicate(self):
self.object_content_type = get_object_or_404( return _(
ContentType, app_label=self.kwargs['app_label'], 'An ACL for "%(object)s" using role "%(role)s" already exists. '
model=self.kwargs['model'] 'Edit that ACL entry instead.'
) ) % {'object': self.get_external_object(), 'role': self.object.role}
try: def get_external_object_queryset(self):
self.content_object = self.object_content_type.get_object_for_this_type( # Here we get a queryset the object model for which an ACL will be
pk=self.kwargs['object_id'] # created.
) return self.get_content_type().get_all_objects_for_this_type()
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_extra_context(self): def get_extra_context(self):
return { return {
'object': self.content_object, 'object': self.get_external_object(),
'title': _( 'title': _(
'New access control lists for: %s' '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): def get_success_url(self):
if self.object.pk: return self.object.get_absolute_url()
return reverse('acls:acl_permissions', args=(self.object.pk,))
else:
return super(ACLCreateView, self).get_success_url()
class ACLDeleteView(SingleObjectDeleteView): class ACLDeleteView(SingleObjectDeleteView):
model = AccessControlList model = AccessControlList
object_permission = permission_acl_edit
def dispatch(self, request, *args, **kwargs): pk_url_kwarg = 'acl_id'
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)
def get_extra_context(self): def get_extra_context(self):
acl = self.get_object()
return { 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(), 'title': _('Delete ACL: %s') % self.get_object(),
} }
def get_post_action_redirect(self): def get_post_action_redirect(self):
instance = self.get_object() instance = self.get_object()
return reverse( return reverse(
'acls:acl_list', args=( 'acls:acl_list', kwargs={
instance.content_type.app_label, 'app_label': instance.content_type.app_label,
instance.content_type.model, instance.object_id 'model_name': instance.content_type.model,
) 'object_id': instance.object_id
}
) )
class ACLListView(SingleObjectListView): class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
def dispatch(self, request, *args, **kwargs): content_type_url_kw_args = {
self.object_content_type = get_object_or_404( 'app_label': 'app_label',
ContentType, app_label=self.kwargs['app_label'], 'model': 'model_name'
model=self.kwargs['model'] }
) external_object_permission = permission_acl_view
external_object_pk_url_kwarg = 'object_id'
try: def get_external_object_queryset(self):
self.content_object = self.object_content_type.get_object_for_this_type( # Here we get a queryset the object model for which an ACL will be
pk=self.kwargs['object_id'] # created.
) return self.get_content_type().get_all_objects_for_this_type()
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_extra_context(self): def get_extra_context(self):
return { return {
@@ -141,7 +122,9 @@ class ACLListView(SingleObjectListView):
'no_results_icon': icon_acl_list, 'no_results_icon': icon_acl_list,
'no_results_main_link': link_acl_create.resolve( 'no_results_main_link': link_acl_create.resolve(
context=RequestContext( context=RequestContext(
self.request, {'resolved_object': self.content_object} self.request, {
'resolved_object': self.get_external_object()
}
) )
), ),
'no_results_title': _( 'no_results_title': _(
@@ -149,116 +132,98 @@ class ACLListView(SingleObjectListView):
), ),
'no_results_text': _( 'no_results_text': _(
'ACL stands for Access Control List and is a precise method ' '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): def get_source_queryset(self):
return AccessControlList.objects.filter( return self.get_external_object().acls.all()
content_type=self.object_content_type,
object_id=self.content_object.pk
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
) )
# Group permissions by namespace
class ACLPermissionsView(AssignRemoveView): for permission in object_list:
grouped = True namespaces_dictionary.setdefault(
left_list_title = _('Available permissions') permission.volatile_permission.namespace.label,
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
) )
) namespaces_dictionary[permission.volatile_permission.namespace.label].append(
(permission.pk, force_text(permission))
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)
) )
return results # Sort permissions by their translatable namespace label
return sorted(namespaces_dictionary.items())
def add(self, item): def get_actions_extra_kwargs(self):
permission = get_object_or_404(StoredPermission, pk=item) return {'_user': self.request.user}
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_disabled_choices(self): def get_disabled_choices(self):
""" """
Get permissions from a parent's acls but remove the permissions we Get permissions from a parent's ACLs or directly granted to the role.
already hold for this object We return a list since that is what the form widget's can process.
""" """
return map( return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
str, set(
self.get_object().get_inherited_permissions().values_list(
'pk', flat=True
)
).difference(
self.get_object().permissions.values_list('pk', flat=True)
)
)
def get_extra_context(self): def get_extra_context(self):
return { return {
'object': self.get_object().content_object, 'acl': self.main_object,
'title': _('Role "%(role)s" permission\'s for "%(object)s"') % { 'object': self.main_object.content_object,
'role': self.get_object().role, 'navigation_object_list': ('object', 'acl'),
'object': self.get_object().content_object, '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): def get_list_added_help_text(self):
""" if self.main_object.get_inherited_permissions():
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():
return _( 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): return (
Permission.refresh() queryset_acl | self.main_object.get_inherited_permissions()
return ACLPermissionsView.generate_choices(self.get_available_list()) ).distinct()
def remove(self, item): def get_secondary_object_source_queryset(self):
permission = get_object_or_404(StoredPermission, pk=item) return ModelPermission.get_for_instance(
self.get_object().permissions.remove(permission) instance=self.main_object.content_object
)
def right_list(self):
return ACLPermissionsView.generate_choices(self.get_granted_list())

View File

@@ -7,10 +7,10 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from document_states.classes import WorkflowAction from mayan.apps.document_states.classes import WorkflowAction
from permissions.classes import Permission from mayan.apps.permissions.classes import Permission
from permissions.models import Role from mayan.apps.permissions.models import Role
from .classes import ModelPermission from .classes import ModelPermission
from .permissions import permission_acl_edit from .permissions import permission_acl_edit
@@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction):
try: try:
AccessControlList.objects.check_access( 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: except Exception as exception:
raise ValidationError(exception) raise ValidationError(exception)
@@ -98,7 +99,9 @@ class GrantAccessAction(WorkflowAction):
def get_form_schema(self, *args, **kwargs): def get_form_schema(self, *args, **kwargs):
self.fields['content_type']['kwargs']['queryset'] = ModelPermission.get_classes(as_content_type=True) 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) return super(GrantAccessAction, self).get_form_schema(*args, **kwargs)
def get_execute_data(self): def get_execute_data(self):

View File

@@ -1,3 +1,3 @@
from __future__ import unicode_literals from __future__ import unicode_literals
default_app_config = 'appearance.apps.AppearanceApp' default_app_config = 'mayan.apps.appearance.apps.AppearanceApp'

View File

@@ -2,13 +2,13 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig from mayan.apps.common import MayanAppConfig
from .licenses import * # NOQA from .licenses import * # NOQA
class AppearanceApp(MayanAppConfig): class AppearanceApp(MayanAppConfig):
name = 'appearance' name = 'mayan.apps.appearance'
verbose_name = _('Appearance') verbose_name = _('Appearance')
def ready(self): def ready(self):

View File

@@ -23,11 +23,38 @@ class FontAwesomeDriver(IconDriver):
self.symbol = symbol self.symbol = symbol
def render(self): def render(self):
return get_template(self.template_name).render( return get_template(template_name=self.template_name).render(
context={'symbol': self.symbol} 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): class FontAwesomeCSSDriver(IconDriver):
name = 'fontawesomecss' name = 'fontawesomecss'
template_name = 'appearance/icons/font_awesome_css.html' template_name = 'appearance/icons/font_awesome_css.html'
@@ -36,11 +63,37 @@ class FontAwesomeCSSDriver(IconDriver):
self.css_classes = css_classes self.css_classes = css_classes
def render(self): 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} 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): class Icon(object):
def __init__(self, driver_name, **kwargs): def __init__(self, driver_name, **kwargs):
self.driver = IconDriver.get(name=driver_name)(**kwargs) self.driver = IconDriver.get(name=driver_name)(**kwargs)
@@ -49,5 +102,8 @@ class Icon(object):
return self.driver.render(**kwargs) return self.driver.render(**kwargs)
IconDriver.register(driver_class=FontAwesomeDriver)
IconDriver.register(driver_class=FontAwesomeCSSDriver) 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)

View File

@@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from common.classes import Package from mayan.apps.common.classes import Package
Package(label='Bootstrap', license_text=''' Package(label='Bootstrap', license_text='''
The MIT License (MIT) The MIT License (MIT)

View File

@@ -1 +1 @@
DEFAULT_MAXIMUM_TITLE_LENGTH = 80 DEFAULT_MAXIMUM_TITLE_LENGTH = 120

View File

@@ -2,11 +2,12 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ 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 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( setting_max_title_length = namespace.add_setting(
default=DEFAULT_MAXIMUM_TITLE_LENGTH, default=DEFAULT_MAXIMUM_TITLE_LENGTH,
global_name='APPEARANCE_MAXIMUM_TITLE_LENGTH', help_text=_( global_name='APPEARANCE_MAXIMUM_TITLE_LENGTH', help_text=_(

View File

@@ -49,10 +49,6 @@
url('../fonts/Lato_400italic.ttf') format('truetype'); url('../fonts/Lato_400italic.ttf') format('truetype');
} }
body {
padding-top: 70px;
}
.navbar-brand { .navbar-brand {
font-family: "IM Fell English SC", serif; font-family: "IM Fell English SC", serif;
} }
@@ -71,7 +67,7 @@ body {
} }
#carousel-container { #carousel-container {
overflow-x: scroll; height: 500px; overflow: scroll; height: 100%;
} }
#carousel-container img { #carousel-container img {
@@ -142,11 +138,7 @@ hr {
} }
.radio ul li { .radio ul li {
list-style-type:none; list-style-type: none;
}
a i {
padding-right: 3px;
} }
.dashboard-widget { .dashboard-widget {
@@ -334,3 +326,209 @@ a i {
transform: rotate(360deg); 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;
}
}

View File

@@ -6,7 +6,8 @@ var MayanAppClass = MayanApp;
var partialNavigation = new PartialNavigation({ var partialNavigation = new PartialNavigation({
initialURL: initialURL, initialURL: initialURL,
disabledAnchorClasses: ['disabled'], disabledAnchorClasses: [
'btn-multi-item-action', 'disabled', 'pagination-disabled'
],
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'], excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess],
}); });

View File

@@ -1,121 +1,76 @@
'use strict'; 'use strict';
class MayanApp { class MayanApp {
constructor (parameters) { constructor (options) {
var self = this; var self = this;
parameters = parameters || {} options = options || {
ajaxMenusOptions: []
}
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.ajaxExecuting = false; this.ajaxExecuting = false;
this.ajaxMenusOptions = [ this.ajaxMenusOptions = options.ajaxMenusOptions;
{
app: this,
interval: 5000,
menuSelector: '#main-menu',
url: apiTemplateMainMenuURL,
}
];
this.ajaxMenuHashes = {}; this.ajaxMenuHashes = {};
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.window = $(window); this.window = $(window);
} }
// Class methods and variables // Class methods and variables
static mayanNotificationBadge (options, data) { static countChecked() {
// Callback to add the notifications count inside a badge markup var checkCount = $('.check-all-slave:checked').length;
var notifications = data[options.attributeName];
if (notifications > 0) { if (checkCount) {
// Save the original link text before adding the initial badge markup $('#multi-item-title').hide();
if (!options.element.data('mn-saved-text')) { $('#multi-item-actions').show();
options.element.data('mn-saved-text', options.element.html());
}
options.element.html(
options.element.data('mn-saved-text') + ' <span class="badge">' + notifications + '</span>'
);
} else { } else {
if (options.element.data('mn-saved-text')) { $('#multi-item-title').show();
// If there is a saved original link text, restore it $('#multi-item-actions').hide();
options.element.html(
options.element.data('mn-saved-text')
);
}
} }
} }
static MultiObjectFormProcess ($form, options) { static setupMultiItemActions () {
/* $('body').on('change', '.check-all-slave', function () {
* ajaxForm callback to add the external item checkboxes to the MayanApp.countChecked();
* submitted form });
*/
if ($form.hasClass('form-multi-object-action')) { $('body').on('click', '.btn-multi-item-action', function (event) {
// Turn form data into an object var id_list = [];
var formArray = $form.serializeArray().reduce(function (obj, item) { $('.check-all-slave:checked').each(function (index, value) {
obj[item.name] = item.value; //Split the name (ie:"pk_200") and extract only the ID
return obj; 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 static setupNavBarState () {
$('.form-multi-object-action-checkbox:checked').each(function() { $('body').on('click', '.a-main-menu-accordion-link', function (event) {
var $this = $(this); console.log('ad');
formArray[$this.attr('name')] = $this.attr('value'); $('.a-main-menu-accordion-link').each(function (index, value) {
$(this).parent().removeClass('active');
}); });
// Set the form data as the data to send $(this).parent().addClass('active');
options.data = formArray; });
}
} }
static tagSelectionTemplate (tag, container) { static updateNavbarState () {
var $tag = $( var uri = new URI(window.location.hash);
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>' var uriFragment = uri.fragment();
); $('.a-main-menu-accordion-link').each(function (index, value) {
container[0].style.background = tag.element.dataset.color; if (value.pathname === uriFragment) {
return $tag; $(this).closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
} $(this).parent().addClass('active');
}
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;
} }
// Instance methods // 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 () { callbackAJAXSpinnerUpdate () {
if (this.ajaxExecuting) { if (this.ajaxExecuting) {
$(this.ajaxSpinnerSeletor).fadeIn(50); $(this.ajaxSpinnerSeletor).fadeIn(50);
@@ -132,10 +87,10 @@ class MayanApp {
if ((menuHash === undefined) || (menuHash !== data.hex_hash)) { if ((menuHash === undefined) || (menuHash !== data.hex_hash)) {
$(options.menuSelector).html(data.html); $(options.menuSelector).html(data.html);
options.app.ajaxMenuHashes[data.name] = data.hex_hash;
if (options.callback !== undefined) { if (options.callback !== undefined) {
options.callback(); options.callback();
} }
options.app.ajaxMenuHashes[data.name] = data.hex_hash;
} }
}, },
url: options.url, url: options.url,
@@ -214,35 +169,24 @@ class MayanApp {
} }
initialize () { initialize () {
this.setupAJAXPeriodicWorkers(); var self = this;
this.setupAJAXSpinner(); this.setupAJAXSpinner();
this.setupAutoSubmit(); this.setupFormHotkeys();
this.setupFullHeightResizing(); this.setupFullHeightResizing();
this.setupItemsSelector(); this.setupItemsSelector();
MayanApp.setupMultiItemActions();
this.setupNavbarCollapse(); this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor(); this.setupNewWindowAnchor();
$.each(this.ajaxMenusOptions, function(index, value) { $.each(this.ajaxMenusOptions, function(index, value) {
value.app = self;
app.doRefreshAJAXMenu(value); app.doRefreshAJAXMenu(value);
}); });
this.setupPanelSelection();
partialNavigation.initialize(); 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 () { setupAJAXSpinner () {
var self = this; var self = this;
@@ -263,12 +207,19 @@ class MayanApp {
}); });
} }
setupAutoSubmit () { setupFormHotkeys () {
$('body').on('change', '.select-auto-submit', function () { $('body').on('keypress', '.form-hotkey-enter', function (e) {
if ($(this).val()) { if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
$(this.form).trigger('submit'); $(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 () { setupFullHeightResizing () {
@@ -286,9 +237,22 @@ class MayanApp {
app.lastChecked = null; app.lastChecked = null;
$('body').on('click', '.check-all', function (event) { $('body').on('click', '.check-all', function (event) {
var $this = $(this);
var checked = $(event.target).prop('checked'); var checked = $(event.target).prop('checked');
var $checkBoxes = $('.check-all-slave'); 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.prop('checked', checked);
$checkBoxes.trigger('change'); $checkBoxes.trigger('change');
}); });
@@ -298,6 +262,7 @@ class MayanApp {
app.lastChecked = this; app.lastChecked = this;
return; return;
} }
if(e.shiftKey) { if(e.shiftKey) {
var $checkBoxes = $('.check-all-slave'); 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 () { setupScrollView () {
$('.scrollable').scrollview(); $('.scrollable').scrollview();
} }
@@ -343,12 +360,6 @@ class MayanApp {
dropdownAutoWidth: true, dropdownAutoWidth: true,
width: '100%' width: '100%'
}); });
$('.select2-tags').select2({
templateSelection: MayanApp.tagSelectionTemplate,
templateResult: MayanApp.tagResultTemplate,
width: '100%'
});
} }
resizeFullHeight () { resizeFullHeight () {

View File

@@ -6,23 +6,24 @@ class MayanImage {
this.load(); this.load();
} }
static intialize () { static intialize (options) {
var app = this; this.options = options || {};
this.options.templateInvalidDocument = this.options.templateInvalidDocument || '<span>Error loading document image</span>';
this.fancybox = $().fancybox({ $().fancybox({
animationDuration : 300,
buttons : [
'fullScreen',
'close',
],
selector: 'a.fancybox',
afterShow: function (instance, current) { afterShow: function (instance, current) {
$('a.a-caption').on('click', function(event) { $('a.a-caption').on('click', function(event) {
instance.close(true); instance.close(true);
}); });
}, },
animationEffect: 'fade',
animationDuration: 100,
buttons : [
'fullScreen',
'close',
],
infobar: true, infobar: true,
selector: 'a.fancybox',
}); });
$('img.lazy-load').lazyload({ $('img.lazy-load').lazyload({
@@ -42,46 +43,53 @@ class MayanImage {
$('.lazy-load').one('load', function() { $('.lazy-load').one('load', function() {
$(this).hide(); $(this).hide();
$(this).fadeIn(300); $(this).show();
$(this).siblings('.spinner-container').remove(); $(this).siblings('.spinner-container').remove();
$(this).removeClass('lazy-load pull-left'); $(this).removeClass('lazy-load pull-left');
clearTimeout(MayanImage.timer); clearTimeout(MayanImage.timer);
MayanImage.timer = setTimeout(MayanImage.timerFunction, 100); MayanImage.timer = setTimeout(MayanImage.timerFunction, 250);
}); });
$('.lazy-load-carousel').one('load', function() { $('.lazy-load-carousel').one('load', function() {
$(this).hide(); $(this).hide();
$(this).fadeIn(300); $(this).show();
$(this).siblings('.spinner-container').remove(); $(this).siblings('.spinner-container').remove();
$(this).removeClass('lazy-load-carousel pull-left'); $(this).removeClass('lazy-load-carousel pull-left');
}); });
} }
static timerFunction () { static timerFunction () {
$.fn.matchHeight._maintainScroll = true;
$.fn.matchHeight._update(); $.fn.matchHeight._update();
} }
load () { load () {
var self = this; var self = this;
var container = this.element.parent().parent().parent(); var container = this.element.parent().parent().parent();
var dataURL = this.element.attr('data-url');
this.element.attr('src', this.element.attr('data-url')); if (dataURL === '') {
this.element.on('error', function() { container.html(MayanImage.options.templateInvalidDocument);
// Check the .complete property to see if it is a real error } else {
// or it was a cached image this.element.attr('src', dataURL);
if (this.complete === false) { setTimeout(function () {
// It is a cached image, set the src attribute to trigger self.element.on('error', function () {
// it's display. // Check the .complete property to see if it is a real
this.src = this.src; // error or it was a cached image
} else { if (this.complete === false) {
container.html(MayanImage.templateInvalidDocument); // It is a cached image, set the src attribute to
} // trigger it's display.
}); this.src = dataURL;
} else {
$.fn.matchHeight._maintainScroll = true; container.html(
MayanImage.options.templateInvalidDocument
);
}
});
}, 1);
}
}; };
} }
MayanImage.templateInvalidDocument = $('#template-invalid-document').html();
MayanImage.timer = setTimeout(null); MayanImage.timer = setTimeout(null);
$.fn.matchHeight._maintainScroll = true;

View File

@@ -160,7 +160,21 @@ class PartialNavigation {
*/ */
if (djangoDEBUG) { 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 { } else {
if (jqXHR.status == 0) { if (jqXHR.status == 0) {
$('#modal-server-error .modal-body').html($('#template-error').html()); $('#modal-server-error .modal-body').html($('#template-error').html());

View File

@@ -4,6 +4,11 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "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": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",

View File

@@ -10,6 +10,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "=5.6.3",
"bootstrap": "=3.3.7", "bootstrap": "=3.3.7",
"bootswatch": "=3.3.7", "bootswatch": "=3.3.7",
"jquery": "=3.3.1", "jquery": "=3.3.1",

View File

@@ -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.**

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

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