Compare commits

..

253 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
Manoel Brunnen
5c9ff90d28 Fix libssl-dev dependency installation
The apt package informations have already been removed at this point.
Also, this dependency is not armhf specific.
2019-02-04 11:24:50 +01: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
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
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
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
557 changed files with 21970 additions and 15531 deletions

View File

@@ -1,5 +1,6 @@
4.0 (2019-XX-XX)
================
<<<<<<< HEAD
- Documents: Add a server side template for invalid documents.
The new template can be accessed via the templates API.
Improve the way invalid documents are detected in the JavaScript
@@ -213,6 +214,49 @@
- Event handler to highlight panels when selected.
- Improve duplicated document display.
- Filter document duplicted count by access.
- Updated the tags app to comply with MERCs 5 and 6.
- The tags app permission workflow is now reciprocal. In order
to attach a tag, the user's role will need the tag attach
permissions for both, the document and the tag.
- Refactor and optimize the access control computation. Most of
the computation has been moved to the database instead of doing
filtering in Python. The refactor added cascading access checking
in preparation for nested cabinet access control and the removal
of the permission proxy support which is now redundant.
- Remove the permissions to grant or revoke a permission to a role.
The instead the role edit permission is used.
- Add a test mixin to generate random model primary keys.
- Add support for checkout and check in multiple documents at
the same time.
- Move file and storage code to the storage app. The setting
COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY.
- To lower memory usage and reduce memory leaks, the entire
entire converter class is no longer cached and instead loaded
on demand. This allows the garbage collector to clear the memory
used.
- 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.
- 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.
- Increate the default maximum title lenght to 120 characters.
- In the search API, 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=
- Remove django-mathfilters from requirements. These tags
are provided by default by Jinja2 template engine
(http://jinja.pocoo.org/docs/2.10/templates/#math).
- Reject emails attachments of size 0. Thanks to Robert Schoeftner
(@robert.schoeftner)for the report and solution. GitLab issue #574.
- Fix multiple tag selection wizard step.
3.1.9 (2018-11-01)

View File

@@ -62,15 +62,16 @@ clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing
test:
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
test-all:
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
test-launch-postgres:
@docker rm -f test-postgres || true

View File

@@ -1445,7 +1445,7 @@ sudo -u mayan \
dialog --infobox "Preparing static files" 3 70
sudo -u mayan \
MAYAN_MEDIA_ROOT=$MAYAN_MEDIA_ROOT \
$MAYAN_BIN collectstatic --noinput > /dev/null
$MAYAN_BIN preparestatic --noinput > /dev/null
# Create supervisor file for gunicorn (frontend), 3 background workers, and the scheduler for periodic tasks
cat > /etc/supervisor/conf.d/mayan.conf <<EOF

View File

@@ -161,7 +161,7 @@ priority = 998
EOF
echo -e "\n -> Collecting the static files \n"
mayan-edms.py collectstatic --noinput
mayan-edms.py preparestatic --noinput
echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R

View File

@@ -48,6 +48,7 @@ apt-get install -y --no-install-recommends \
supervisor \
tesseract-ocr \
zlib1g-dev \
libssl-dev \
&& \
apt-get clean autoclean && \
apt-get autoremove --purge -y && \
@@ -68,7 +69,6 @@ ln -s /usr/lib/aarch64-linux-gnu/libjpeg.so /usr/lib/ \
# Pillow can't find zlib or libjpeg on armv7l (ODROID HC1)
RUN if [ "$(uname -m)" = "armv7l" ]; then \
apt-get install libssl-dev -y && \
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ && \
ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
; fi
@@ -132,11 +132,11 @@ COPY --from=BUILDER_IMAGE /code/docker/version .
RUN chown -R mayan:mayan $PROJECT_INSTALL_DIR
# Install build Mayan EDMS
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir *.whl && \
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir --no-use-pep517 *.whl && \
rm *.whl
# Install Python clients for librabbitmq, MySQL, PostgreSQL, REDIS
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir librabbitmq==1.6.1 mysql-python==1.2.5 psycopg2==2.7.3.2 redis==2.10.6
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir --no-use-pep517 librabbitmq==1.6.1 mysql-python==1.2.5 psycopg2==2.7.3.2 redis==2.10.6
# Setup supervisor
COPY docker/etc/supervisor/mayan.conf /etc/supervisor/conf.d

View File

@@ -55,13 +55,13 @@ chown mayan:mayan /var/lib/mayan -R
initialize() {
echo "mayan: initialize()"
su mayan -c "${MAYAN_BIN} initialsetup --force"
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear"
su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
}
upgrade() {
echo "mayan: upgrade()"
su mayan -c "${MAYAN_BIN} performupgrade"
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear"
su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
}
start() {

View File

@@ -82,7 +82,7 @@ Collect the static files:
::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py collectstatic --noinput
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
--------------------------------------------------------------------
@@ -244,7 +244,7 @@ Collect the static files:
::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py collectstatic --noinput
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
Create the RabbitMQ user and vhost:
-----------------------------------

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
provides general guidelines or information to the Mayan EDMS community,
but does not propose a new feature. Informational MERCs do not
necessarily represent a community consensus or
recommendation, so users and implementers are free to ignore
Informational MERCs or follow their advice.
necessarily represent a community consensus or recommendation, so users
and implementers are free to ignore Informational MERCs or follow their
advice.
3. A **Process** MERC describes a process surrounding Mayan EDMS, or
proposes a change to (or an event in) a process. Process MERCs are

View File

@@ -1,6 +1,6 @@
=====================
====================
MERC 2: Test writing
=====================
====================
:MERC: 2
: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/0002-test-writing
../mercs/0003-using-javascript-libraries
../mercs/0005-explicit-arguments
../mercs/0006-lower-information-disclose
Draft
-----
@@ -49,3 +51,5 @@ Feature
../mercs/0002-test-writing
../mercs/0003-using-javascript-libraries
../mercs/0005-explicit-arguments
../mercs/0006-lower-information-disclose

View File

@@ -63,5 +63,5 @@ Changes needed:
the Role model's permissions many to many field.
4. Update the ``AccessControlList`` models roles field to point to the group
models.
5. Update the role checks in the ``check_access`` and ``filter_by_access``
5. Update the role checks in the ``check_access`` and ``restrict_queryset``
``AccessControlList`` model manager methods.

View File

@@ -7,6 +7,24 @@ 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
--------
@@ -63,7 +81,7 @@ Migrate existing database schema with::
Add new static media::
$ mayan-edms.py collectstatic --noinput
$ mayan-edms.py preparestatic --noinput
The upgrade procedure is now complete.

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
testing. For production deployments the management command::
$ mayan-edms.py collectstatic
$ mayan-edms.py preparestatic
should be used and the resulting static folder served from a webserver.
For more information check the

View File

@@ -1,202 +1,121 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import AccessControlList
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import (
AccessControlListPermissionSerializer, AccessControlListSerializer,
WritableAccessControlListPermissionSerializer,
WritableAccessControlListSerializer
from mayan.apps.common.mixins import ContentTypeViewMixin
from mayan.apps.permissions.serializers import (
PermissionSerializer, RolePermissionAddRemoveSerializer
)
from mayan.apps.rest_api.mixins import ExternalObjectAPIViewSetMixin
from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import AccessControlListSerializer
class APIObjectACLListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the object's access control lists
post: Create a new access control list for the selected object.
"""
def get_content_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk']
)
if self.request.method == 'GET':
permission_required = permission_acl_view
else:
permission_required = permission_acl_edit
AccessControlList.objects.check_access(
permissions=permission_required, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_content_object().acls.all()
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
context = super(APIObjectACLListView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'content_object': self.get_content_object(),
}
)
return context
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
return super(APIObjectACLListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListSerializer
else:
return WritableAccessControlListSerializer
class APIObjectACLView(generics.RetrieveDestroyAPIView):
"""
delete: Delete the selected access control list.
get: Returns the details of the selected access control list.
"""
class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet):
content_type_url_kw_args = {
'app_label': 'app_label',
'model': 'model_name'
}
external_object_pk_url_kwarg = 'object_id'
lookup_url_kwarg = 'acl_id'
serializer_class = AccessControlListSerializer
def get_content_object(self):
if self.request.method == 'GET':
permission_required = permission_acl_view
else:
permission_required = permission_acl_edit
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.validated_data.update(
{
'object_id': self.external_object.pk,
'content_type': self.get_content_type(),
}
)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk']
)
AccessControlList.objects.check_access(
permissions=permission_required, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_content_object().acls.all()
class APIObjectACLPermissionListView(generics.ListCreateAPIView):
"""
get: Returns the access control list permission list.
post: Add a new permission to the selected access control list.
"""
def get_acl(self):
return get_object_or_404(
klass=self.get_content_object().acls, pk=self.kwargs['pk']
)
def get_content_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk']
)
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_acl().permissions.all()
def get_serializer(self, *args, **kwargs):
if not self.request:
def get_external_object_permission(self):
action = getattr(self, 'action', None)
if action is None:
return None
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListPermissionSerializer
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
return permission_acl_view
else:
return WritableAccessControlListPermissionSerializer
return permission_acl_edit
def get_serializer_context(self):
context = super(APIObjectACLPermissionListView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'acl': self.get_acl(),
}
)
return context
class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
"""
delete: Remove the permission from the selected access control list.
get: Returns the details of the selected access control list permission.
"""
lookup_url_kwarg = 'permission_pk'
serializer_class = AccessControlListPermissionSerializer
def get_acl(self):
return get_object_or_404(
klass=self.get_content_object().acls, pk=self.kwargs['pk']
)
def get_content_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk']
)
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=self.request.user,
obj=content_object
)
return content_object
def get_external_object_queryset(self):
# Here we get a queryset the object model for which the event
# will be accessed.
return self.get_content_type().get_all_objects_for_this_type()
def get_queryset(self):
return self.get_acl().permissions.all()
return self.get_external_object().acls.all()
def get_serializer_context(self):
context = super(APIObjectACLPermissionView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'acl': self.get_acl(),
}
)
@action(
detail=True, lookup_url_kwarg='acl_id', methods=('post',),
serializer_class=RolePermissionAddRemoveSerializer,
url_name='permission-add', url_path='permissions/add'
)
def permission_add(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.permissions_add(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, headers=headers, status=status.HTTP_200_OK
)
return context
@action(
detail=True, lookup_url_kwarg='acl_id',
serializer_class=PermissionSerializer, url_name='permission-list',
url_path='permissions'
)
def permission_list(self, request, *args, **kwargs):
queryset = self.get_object().permissions.all()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
@action(
detail=True, lookup_url_kwarg='acl_id',
serializer_class=PermissionSerializer,
url_name='permission-inherited-list', url_path='permissions/inherited'
)
def permission_inherited_list(self, request, *args, **kwargs):
queryset = self.get_object().get_inherited_permissions()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
@action(
detail=True, lookup_url_kwarg='acl_id',
methods=('post',), serializer_class=RolePermissionAddRemoveSerializer,
url_name='permission-remove', url_path='permissions/remove'
)
def permission_remove(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.permissions_remove(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, headers=headers, status=status.HTTP_200_OK
)

View File

@@ -2,9 +2,15 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar
from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
from mayan.apps.events import ModelEventType
from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list
)
from mayan.apps.navigation import SourceColumn
from .classes import ModelPermission
from .events import event_acl_created, event_acl_edited
from .links import link_acl_create, link_acl_delete, link_acl_permissions
@@ -18,21 +24,33 @@ class ACLsApp(MayanAppConfig):
def ready(self):
super(ACLsApp, self).ready()
from actstream import registry
AccessControlList = self.get_model('AccessControlList')
AccessControlList = self.get_model(model_name='AccessControlList')
SourceColumn(
source=AccessControlList, label=_('Role'), attribute='role'
ModelEventType.register(
event_types=(event_acl_created, event_acl_edited),
model=AccessControlList
)
ModelPermission.register_inheritance(
model=AccessControlList, related='content_object',
)
SourceColumn(
source=AccessControlList, label=_('Permissions'),
attribute='get_permission_titles'
attribute='role', is_identifier=True, is_sortable=True,
source=AccessControlList
)
menu_object.bind_links(
links=(link_acl_permissions, link_acl_delete),
links=(
link_acl_permissions, link_acl_delete,
link_events_for_object,
link_object_event_types_user_subcriptions_list
),
sources=(AccessControlList,)
)
menu_sidebar.bind_links(
menu_secondary.bind_links(
links=(link_acl_create,), sources=('acls:acl_list',)
)
registry.register(AccessControlList)

View File

@@ -40,25 +40,15 @@ class ModelPermission(object):
app_label='permissions', model_name='StoredPermission'
)
permissions = []
class_permissions = cls.get_for_class(klass=type(instance))
if class_permissions:
permissions.extend(class_permissions)
proxy = cls._proxies.get(type(instance))
if proxy:
permissions.extend(cls._registry.get(proxy))
permissions = cls.get_for_class(klass=type(instance))
pks = [
permission.stored_permission.pk for permission in set(permissions)
permission.stored_permission.pk for permission in permissions
]
return StoredPermission.objects.filter(pk__in=pks)
@classmethod
def get_inheritance(cls, model):
def get_inheritances(cls, model):
return cls._inheritances[model]
@classmethod
@@ -79,7 +69,8 @@ class ModelPermission(object):
@classmethod
def register_inheritance(cls, model, related):
cls._inheritances[model] = related
cls._inheritances.setdefault(model, [])
cls._inheritances[model].append(related)
@classmethod
def register_proxy(cls, source, model):

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

@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_acl_delete = Icon(driver_name='fontawesome', symbol='times')
icon_acl_list = Icon(driver_name='fontawesome', symbol='lock')
icon_acl_new = Icon(
driver_name='fontawesome-dual', primary_symbol='lock',

View File

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

View File

@@ -1,15 +1,21 @@
from __future__ import absolute_import, unicode_literals
import logging
import operator
import warnings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.db.models import CharField, Value as V, Q
from django.db.models.functions import Concat
from django.http import Http404
from mayan.apps.common.utils import resolve_attribute, return_related
from mayan.apps.common.utils import (
get_related_field, resolve_attribute, return_related
)
from mayan.apps.common.warnings import InterfaceWarning
from mayan.apps.permissions import Permission
from mayan.apps.permissions.models import StoredPermission
@@ -24,200 +30,189 @@ class AccessControlListManager(models.Manager):
Implement a 3 tier permission system, involving a permissions, an actor
and an object
"""
def check_access(self, permissions, user, obj, related=None):
if user.is_superuser or user.is_staff:
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" as superuser '
'or staff', permissions, obj, user
def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None):
"""
This method does the bulk of the work. It generates filters for the
AccessControlList model to determine if there are ACL entries for the
members of the queryset's model provided.
"""
# Determine which of the cases we need to address
# 1: No related field
# 2: Related field
# 3: Related field that is Generic Foreign Key
# 4: No related field, but has an inherited related field, solved by
# recursion, branches to #2 or #3.
# 5: Inherited field of a related field
# -- Not addressed yet --
# 6: Inherited field of a related field that is Generic Foreign Key
result = []
if related_field_name:
related_field = get_related_field(
model=queryset.model, related_field_name=related_field_name
)
return True
try:
return Permission.check_permissions(
requester=user, permissions=permissions
)
except PermissionDenied:
try:
stored_permissions = [
permission.stored_permission for permission in permissions
]
except TypeError:
# Not a list of permissions, just one
stored_permissions = (permissions.stored_permission,)
if related:
obj = resolve_attribute(obj=obj, attribute=related)
try:
parent_accessor = ModelPermission.get_inheritance(
model=obj._meta.model
)
except AttributeError:
# AttributeError means non model objects: ie Statistics
# These can't have ACLs so we raise PermissionDenied
raise PermissionDenied(_('Insufficient access for: %s') % obj)
except KeyError:
pass
else:
try:
return self.check_access(
obj=getattr(obj, parent_accessor),
permissions=permissions, user=user
if isinstance(related_field, GenericForeignKey):
# Case 3: Generic Foreign Key, multiple ContentTypes + object
# id combinations
content_type_object_id_queryset = queryset.annotate(
ct_fk_combination=Concat(
related_field.ct_field, V('-'), related_field.fk_field,
output_field=CharField()
)
except AttributeError:
# Has no such attribute, try it as a related field
try:
return self.check_access(
obj=return_related(
instance=obj, related_field=parent_accessor
), permissions=permissions, user=user
)
except PermissionDenied:
pass
except PermissionDenied:
pass
).values('ct_fk_combination')
user_roles = []
for group in user.groups.all():
for role in group.roles.all():
if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))):
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL',
permissions, obj, user, role
)
return True
acl_filter = self.annotate(
ct_fk_combination=Concat(
'content_type', V('-'), 'object_id', output_field=CharField()
)
).filter(
permissions=stored_permission, role__groups__user=user,
ct_fk_combination__in=content_type_object_id_queryset
).values('object_id')
user_roles.append(role)
field_lookup = 'object_id__in'
if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
logger.debug(
'Permissions "%s" on "%s" denied for user "%s"',
permissions, obj, user
result.append(Q(**{field_lookup: acl_filter}))
else:
# Case 2: Related field of a single type, single ContentType,
# multiple object id
content_type = ContentType.objects.get_for_model(
model=related_field.related_model
)
raise PermissionDenied(ugettext('Insufficient access for: %s') % obj)
field_lookup = '{}_id__in'.format(related_field_name)
acl_filter = self.filter(
content_type=content_type, permissions=stored_permission,
role__groups__user=user
).values('object_id')
result.append(Q(**{field_lookup: acl_filter}))
# Case 5: Related field, has an inherited related field itself
# Bubble up permssion check
# TODO: Add relationship support: OR or AND
# TODO: OR for document pages, version, doc, and types
# TODO: AND for new cabinet levels ACLs
try:
related_field_model_related_fields = ModelPermission.get_inheritances(
model=related_field.related_model
)
except KeyError:
pass
else:
relation_result = []
for related_field_model_related_field_name in related_field_model_related_fields:
related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name)
related_field_inherited_acl_queries = self._get_acl_filters(
queryset=queryset, stored_permission=stored_permission,
user=user, related_field_name=related_field_name
)
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
permissions, obj, user, user_roles
)
relation_result.append(reduce(operator.and_, related_field_inherited_acl_queries))
def filter_by_access(self, permission, user, queryset):
if user.is_superuser or user.is_staff:
logger.debug(
'Unfiltered queryset returned to user "%s" as superuser or staff',
user
)
return queryset
try:
Permission.check_permissions(
requester=user, permissions=(permission,)
)
except PermissionDenied:
user_roles = []
for group in user.groups.all():
for role in group.roles.all():
user_roles.append(role)
result.append(reduce(operator.or_, relation_result))
else:
# Case 1: Original model, single ContentType, multiple object id
content_type = ContentType.objects.get_for_model(model=queryset.model)
field_lookup = 'id__in'
acl_filter = self.filter(
content_type=content_type, permissions=stored_permission,
role__groups__user=user
).values('object_id')
result.append(Q(**{field_lookup: acl_filter}))
# Case 4: Original model, has an inherited related field
try:
parent_accessor = ModelPermission.get_inheritance(
related_fields = ModelPermission.get_inheritances(
model=queryset.model
)
except KeyError:
parent_acl_query = Q()
pass
else:
instance = queryset.first()
if instance:
parent_object = return_related(
instance=instance, related_field=parent_accessor
relation_result = []
for related_field_name in related_fields:
inherited_acl_queries = self._get_acl_filters(
queryset=queryset, stored_permission=stored_permission,
related_field_name=related_field_name, user=user
)
relation_result.append(reduce(operator.and_, inherited_acl_queries))
try:
# Try to see if parent_object is a function
parent_object()
except TypeError:
# Is not a function, try it as a field
parent_content_type = ContentType.objects.get_for_model(
parent_object
)
parent_queryset = self.filter(
content_type=parent_content_type, role__in=user_roles,
permissions=permission.stored_permission
)
parent_acl_query = Q(
**{
'{}__pk__in'.format(
parent_accessor
): parent_queryset.values_list(
'object_id', flat=True
)
}
)
else:
# Is a function. Can't perform Q object filtering.
# Perform iterative filtering.
result = []
for entry in queryset:
try:
self.check_access(permissions=permission, user=user, obj=entry)
except PermissionDenied:
pass
else:
result.append(entry.pk)
result.append(reduce(operator.or_, relation_result))
return queryset.filter(pk__in=result)
else:
parent_acl_query = Q()
return result
# Directly granted access
content_type = ContentType.objects.get_for_model(queryset.model)
acl_query = Q(pk__in=self.filter(
content_type=content_type, role__in=user_roles,
permissions=permission.stored_permission
).values_list('object_id', flat=True))
logger.debug(
'Filtered queryset returned to user "%s" based on roles "%s"',
user, user_roles
)
def check_access(self, obj, permission, user, raise_404=False):
warnings.warn(
'check_access() is deprecated, use restrict_queryset() to '
'produce a queryset from which to .get() the corresponding '
'object in the local code.', InterfaceWarning
)
queryset = self.restrict_queryset(
permission=permission, queryset=obj._meta.default_manager.all(),
user=user
)
return queryset.filter(parent_acl_query | acl_query)
if queryset.filter(pk=obj.pk).exists():
return True
else:
if raise_404:
raise Http404
else:
raise PermissionDenied
def get_inherited_permissions(self, obj, role):
queryset = self._get_inherited_object_permissions(obj=obj, role=role)
queryset = queryset | role.permissions.all()
# Filter the permissions to the ones that apply to the model
queryset = ModelPermission.get_for_instance(
instance=obj
).filter(
pk__in=queryset
)
return queryset
def _get_inherited_object_permissions(self, obj, role):
queryset = StoredPermission.objects.none()
if not obj:
return queryset
def get_inherited_permissions(self, role, obj):
try:
instance = obj.first()
except AttributeError:
instance = obj
else:
if not instance:
return StoredPermission.objects.none()
try:
parent_accessor = ModelPermission.get_inheritance(type(instance))
related_fields = ModelPermission.get_inheritances(
model=type(obj)
)
except KeyError:
return StoredPermission.objects.none()
pass
else:
try:
parent_object = resolve_attribute(
obj=instance, attribute=parent_accessor
)
except AttributeError:
# Parent accessor is not an attribute, try it as a related
# field.
parent_object = return_related(
instance=instance, related_field=parent_accessor
)
content_type = ContentType.objects.get_for_model(parent_object)
try:
return self.get(
role=role, content_type=content_type,
object_id=parent_object.pk
).permissions.all()
except self.model.DoesNotExist:
return StoredPermission.objects.none()
for related_field_name in related_fields:
try:
parent_object = resolve_attribute(
obj=obj, attribute=related_field_name
)
except AttributeError:
# Parent accessor is not an attribute, try it as a related
# field.
parent_object = return_related(
instance=obj, related_field=related_field_name
)
content_type = ContentType.objects.get_for_model(model=parent_object)
try:
queryset = queryset | self.get(
content_type=content_type, object_id=parent_object.pk,
role=role
).permissions.all()
except self.model.DoesNotExist:
pass
def grant(self, permission, role, obj):
queryset = queryset | self._get_inherited_object_permissions(
obj=parent_object, role=role
)
return queryset
def grant(self, obj, permission, role):
class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
if permission not in class_permissions:
raise PermissionNotValidForClass
@@ -230,7 +225,44 @@ class AccessControlListManager(models.Manager):
acl.permissions.add(permission.stored_permission)
def revoke(self, permission, role, obj):
return acl
def restrict_queryset_by_accesses(self, operator, permissions, queryset, user):
result = []
for permission in permissions:
result.append(
self.restrict_queryset(
permission=permission, queryset=queryset, user=user
)
)
return reduce(operator, result)
def restrict_queryset(self, permission, queryset, user):
# Check directly granted permission via a role
try:
Permission.check_user_permission(permission=permission, user=user)
except PermissionDenied:
acl_filters = self._get_acl_filters(
queryset=queryset,
stored_permission=permission.stored_permission, user=user
)
final_query = None
for acl_filter in acl_filters:
if final_query is None:
final_query = acl_filter
else:
final_query = final_query | acl_filter
return queryset.filter(final_query)
else:
# User has direct permission assignment via a role, is superuser or
# is staff. Return the entire queryset.
return queryset
def revoke(self, obj, permission, role):
content_type = ContentType.objects.get_for_model(model=obj)
acl, created = self.get_or_create(
content_type=content_type, object_id=obj.pk,

View File

@@ -1,15 +1,18 @@
from __future__ import absolute_import, unicode_literals
import logging
import operator
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from mayan.apps.permissions.models import Role, StoredPermission
from .events import event_acl_created, event_acl_edited
from .managers import AccessControlListManager
logger = logging.getLogger(__name__)
@@ -29,6 +32,11 @@ class AccessControlList(models.Model):
* Role - Custom role that is being granted a permission. Roles are created
in the Setup menu.
"""
# Multiple inheritance operator types
OPERATOR_AND = operator.and_
OPERATOR_OR = operator.or_
operator_default = OPERATOR_AND
content_type = models.ForeignKey(
on_delete=models.CASCADE, related_name='object_content_type',
to=ContentType
@@ -57,13 +65,17 @@ class AccessControlList(models.Model):
def __str__(self):
return _(
'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"'
'Role "%(role)s" permission\'s for "%(object)s"'
) % {
'permissions': self.get_permission_titles(),
'object': self.content_object,
'role': self.role
'role': self.role,
}
def get_absolute_url(self):
return reverse(
viewname='acls:acl_permissions', kwargs={'acl_id': self.pk}
)
def get_inherited_permissions(self):
return AccessControlList.objects.get_inherited_permissions(
role=self.role, obj=self.content_object
@@ -78,3 +90,33 @@ class AccessControlList(models.Model):
)
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

@@ -7,8 +7,8 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Access control lists'), name='acls')
permission_acl_edit = namespace.add_permission(
name='acl_edit', label=_('Edit ACLs')
label=_('Edit ACLs'), name='acl_edit'
)
permission_acl_view = namespace.add_permission(
name='acl_view', label=_('View ACLs')
label=_('View ACLs'), name='acl_view'
)

View File

@@ -1,206 +1,143 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from mayan.apps.common.serializers import ContentTypeSerializer
from mayan.apps.permissions import Permission
from mayan.apps.permissions.models import Role, StoredPermission
from mayan.apps.permissions.serializers import (
PermissionSerializer, RoleSerializer
)
from mayan.apps.permissions.models import Role
from mayan.apps.permissions.permissions import permission_role_edit
from mayan.apps.permissions.serializers import RoleSerializer
from mayan.apps.rest_api.mixins import ExternalObjectSerializerMixin
from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField
from .models import AccessControlList
class AccessControlListSerializer(serializers.ModelSerializer):
class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True)
permissions_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to the list of permissions for this access '
'control list.'
)
)
role = RoleSerializer(read_only=True)
url = serializers.SerializerMethodField()
permission_add_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-add'
)
permission_list_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-list'
)
permission_list_inherited_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-inherited-list'
)
permission_remove_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-remove'
)
role_id = serializers.CharField(
label=_('Role ID'),
help_text=_(
'Primary key of the role of the ACL that will be created or edited.'
), required=False, write_only=True
)
url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-detail'
)
class Meta:
external_object_model = Role
external_object_pk_field = 'role_id'
external_object_permission = permission_role_edit
fields = (
'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url'
'content_type', 'id', 'object_id', 'permission_add_url',
'permission_list_url', 'permission_list_inherited_url',
'permission_remove_url', 'role', 'role_id',
'url'
)
model = AccessControlList
def get_permissions_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-list', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
)
def get_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
)
class AccessControlListPermissionSerializer(PermissionSerializer):
acl_permission_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to a permission in relation to the '
'access control list to which it is attached. This URL is '
'different than the canonical workflow URL.'
)
)
acl_url = serializers.SerializerMethodField()
def get_acl_permission_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-detail', args=(
self.context['acl'].content_type.app_label,
self.context['acl'].content_type.model,
self.context['acl'].object_id, self.context['acl'].pk,
instance.stored_permission.pk
), request=self.context['request'], format=self.context['format']
)
def get_acl_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', args=(
self.context['acl'].content_type.app_label,
self.context['acl'].content_type.model,
self.context['acl'].object_id, self.context['acl'].pk
), request=self.context['request'], format=self.context['format']
)
class WritableAccessControlListPermissionSerializer(AccessControlListPermissionSerializer):
permission_pk = serializers.CharField(
help_text=_(
'Primary key of the new permission to grant to the access control '
'list.'
), write_only=True
)
class Meta:
fields = ('namespace',)
read_only_fields = ('namespace',)
read_only_fields = ('object_id',)
def create(self, validated_data):
for permission in validated_data['permissions']:
self.context['acl'].permissions.add(permission)
role = self.get_external_object()
return validated_data['permissions'][0]
if role:
validated_data['role'] = role
def validate(self, attrs):
permissions_pk_list = attrs.pop('permission_pk', None)
permissions_result = []
if permissions_pk_list:
for pk in permissions_pk_list.split(','):
try:
permission = Permission.get(pk=pk)
except KeyError:
raise ValidationError(_('No such permission: %s') % pk)
else:
# Accumulate valid stored permission pks
permissions_result.append(permission.pk)
attrs['permissions'] = StoredPermission.objects.filter(
pk__in=permissions_result
)
return attrs
class WritableAccessControlListSerializer(serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True)
permissions_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of permission primary keys to grant to this '
'access control list.'
), required=False
)
permissions_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to the list of permissions for this access '
'control list.'
), read_only=True
)
role_pk = serializers.IntegerField(
help_text=_(
'Primary keys of the role to which this access control list '
'binds to.'
), write_only=True
)
url = serializers.SerializerMethodField()
class Meta:
fields = (
'content_type', 'id', 'object_id', 'permissions_pk_list',
'permissions_url', 'role_pk', 'url'
)
model = AccessControlList
read_only_fields = ('content_type', 'object_id')
def get_permissions_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-list', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
return super(AccessControlListSerializer, self).create(
validated_data=validated_data
)
def get_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
def update(self, instance, validated_data):
role = self.get_external_object()
if role:
validated_data['role'] = role
return super(AccessControlListSerializer, self).update(
instance=instance, validated_data=validated_data
)
def validate(self, attrs):
attrs['content_type'] = ContentType.objects.get_for_model(
self.context['content_object']
)
attrs['object_id'] = self.context['content_object'].pk
try:
attrs['role'] = Role.objects.get(pk=attrs.pop('role_pk'))
except Role.DoesNotExist as exception:
raise ValidationError(force_text(exception))
permissions_pk_list = attrs.pop('permissions_pk_list', None)
permissions_result = []
if permissions_pk_list:
for pk in permissions_pk_list.split(','):
try:
permission = Permission.get(pk=pk)
except KeyError:
raise ValidationError(_('No such permission: %s') % pk)
else:
# Accumulate valid stored permission pks
permissions_result.append(permission.pk)
instance = AccessControlList(**attrs)
try:
instance.full_clean()
except DjangoValidationError as exception:
raise ValidationError(exception)
# Add a queryset of valid stored permissions so that they get added
# after the ACL gets created.
attrs['permissions'] = StoredPermission.objects.filter(
pk__in=permissions_result
)
return attrs

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

@@ -9,16 +9,13 @@ from ..workflow_actions import GrantAccessAction, RevokeAccessAction
class ACLActionTestCase(ActionTestCase):
def setUp(self):
super(ACLActionTestCase, self).setUp()
def test_grant_access_action(self):
action = GrantAccessAction(
form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk,
'roles': [self.role.pk],
'permissions': [permission_document_view.uuid],
'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.pk],
}
)
action.execute(context={'entry_log': self.entry_log})
@@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase):
list(self.document.acls.first().permissions.all()),
[permission_document_view.stored_permission]
)
self.assertEqual(self.document.acls.first().role, self.role)
self.assertEqual(self.document.acls.first().role, self._test_case_role)
def test_revoke_access_action(self):
self.grant_access(
@@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase):
form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk,
'roles': [self.role.pk],
'permissions': [permission_document_view.uuid],
'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.pk],
}
)
action.execute(context={'entry_log': self.entry_log})

View File

@@ -1,203 +1,189 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import status
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import DocumentTestMixin
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
from mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import AccessControlList
from ..permissions import permission_acl_view
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
class ACLAPITestCase(ACLTestMixin, BaseAPITestCase):
def setUp(self):
super(ACLAPITestCase, self).setUp()
self.login_admin_user()
self._setup_test_object()
self._create_test_acl()
self.test_acl.permissions.add(self.test_permission.stored_permission)
self.document_content_type = ContentType.objects.get_for_model(
self.document
def _request_object_acl_list_api_view(self):
return self.get(
viewname='rest_api:object-acl-list',
kwargs=self.test_content_object_view_kwargs
)
def _create_acl(self):
self.acl = AccessControlList.objects.create(
content_object=self.document,
role=self.role
)
def test_object_acl_list_api_view_no_permission(self):
response = self._request_object_acl_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.acl.permissions.add(permission_document_view.stored_permission)
def test_object_acl_list_view(self):
self._create_acl()
response = self.get(
viewname='rest_api:accesscontrollist-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk
)
)
def test_object_acl_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['content_type']['app_label'],
self.document_content_type.app_label
self.test_object_content_type.app_label
)
self.assertEqual(
response.data['results'][0]['role']['label'], TEST_ROLE_LABEL
response.data['results'][0]['role']['label'],
self.test_acl.role.label
)
def test_object_acl_delete_view(self):
self._create_acl()
def _request_acl_delete_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
response = self.delete(
viewname='rest_api:accesscontrollist-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
return self.delete(
viewname='rest_api:object-acl-detail',
kwargs=kwargs
)
def test_object_acl_delete_api_view_with_access(self):
self.expected_content_type = None
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_acl_delete_api_view()
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(AccessControlList.objects.count(), 0)
self.assertTrue(self.test_acl not in AccessControlList.objects.all())
def test_object_acl_detail_view(self):
self._create_acl()
def test_object_acl_delete_api_view_no_permission(self):
response = self._request_acl_delete_api_view()
response = self.get(
viewname='rest_api:accesscontrollist-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(self.test_acl in AccessControlList.objects.all())
def _request_object_acl_detail_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.get(
viewname='rest_api:object-acl-detail',
kwargs=kwargs
)
def test_object_acl_detail_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_detail_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['content_type']['app_label'],
self.document_content_type.app_label
self.test_object_content_type.app_label
)
self.assertEqual(
response.data['role']['label'], TEST_ROLE_LABEL
response.data['role']['label'], self.test_acl.role.label
)
def test_object_acl_permission_delete_view(self):
self._create_acl()
permission = self.acl.permissions.first()
def test_object_acl_detail_api_view_no_permission(self):
response = self._request_object_acl_detail_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.delete(
viewname='rest_api:accesscontrollist-permission-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk,
permission.pk
)
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(self.acl.permissions.count(), 0)
def _request_object_acl_permission_list_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
def test_object_acl_permission_detail_view(self):
self._create_acl()
permission = self.acl.permissions.first()
response = self.get(
viewname='rest_api:accesscontrollist-permission-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk,
permission.pk
)
return self.get(
viewname='rest_api:object-acl-permission-list',
kwargs=kwargs
)
self.assertEqual(
response.data['pk'], permission_document_view.pk
)
def test_object_acl_permission_list_view(self):
self._create_acl()
response = self.get(
viewname='rest_api:accesscontrollist-permission-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
)
def test_object_acl_permission_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['pk'],
permission_document_view.pk
self.test_permission.pk
)
def test_object_acl_permission_list_post_view(self):
self._create_acl()
def test_object_acl_permission_list_api_view_no_permission(self):
response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.post(
viewname='rest_api:accesscontrollist-permission-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
), data={'permission_pk': permission_acl_view.pk}
def _request_object_acl_permission_remove_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.post(
viewname='rest_api:object-acl-permission-remove',
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertQuerysetEqual(
ordered=False, qs=self.acl.permissions.all(), values=(
repr(permission_document_view.stored_permission),
repr(permission_acl_view.stored_permission)
)
def test_object_acl_permission_remove_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_object_acl_permission_remove_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
def test_object_acl_permission_remove_api_view_no_permission(self):
response = self._request_object_acl_permission_remove_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
def _request_object_acl_permission_add_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.post(
viewname='rest_api:object-acl-permission-add',
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
)
def test_object_acl_post_no_permissions_added_view(self):
response = self.post(
viewname='rest_api:accesscontrollist-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk
), data={'role_pk': self.role.pk}
def test_object_acl_permission_add_api_view_with_access(self):
self.test_acl.permissions.clear()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_object_acl_permission_add_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
def test_object_acl_permission_add_api_view_no_permission(self):
self.test_acl.permissions.clear()
response = self._request_object_acl_permission_add_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
def _request_object_acl_inherited_permission_list_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.get(
viewname='rest_api:object-acl-permission-inherited-list',
kwargs=kwargs
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_object_acl_inherited_permission_list_api_view_with_access(self):
self.test_acl.permissions.clear()
self.test_role.grant(permission=self.test_permission)
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_inherited_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
self.document.acls.first().role, self.role
)
self.assertEqual(
self.document.acls.first().content_object, self.document
)
self.assertEqual(
self.document.acls.first().permissions.count(), 0
response.data['results'][0]['pk'],
self.test_permission.pk
)
def test_object_acl_post_with_permissions_added_view(self):
response = self.post(
viewname='rest_api:accesscontrollist-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk
), data={
'role_pk': self.role.pk,
'permissions_pk_list': permission_acl_view.pk
def test_object_acl_inherited_permission_list_api_view_no_permission(self):
self.test_acl.permissions.clear()
self.test_role.grant(permission=self.test_permission)
}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
self.document.acls.first().content_object, self.document
)
self.assertEqual(
self.document.acls.first().role, self.role
)
self.assertEqual(
self.document.acls.first().permissions.first(),
permission_acl_view.stored_permission
)
response = self._request_object_acl_inherited_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

View File

@@ -1,100 +1,84 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from ..links import (
link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions
)
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class ACLsLinksTestCase(GenericDocumentViewTestCase):
def test_document_acl_create_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
auto_create_test_role = False
self.add_test_view(test_object=self.document)
def setUp(self):
super(AccessControlListLinksTestCase, self).setUp()
self._setup_test_object()
def test_object_acl_create_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
self.add_test_view(test_object=self.test_object)
context = self.get_test_view()
resolved_link = link_acl_create.resolve(context=context)
self.assertNotEqual(resolved_link, None)
content_type = ContentType.objects.get_for_model(self.document)
kwargs = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.document.pk
}
self.assertEqual(
resolved_link.url, reverse('acls:acl_create', kwargs=kwargs)
resolved_link.url, reverse(
viewname='acls:acl_create',
kwargs=self.test_content_object_view_kwargs
)
)
def test_document_acl_delete_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
def test_object_acl_delete_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
self.add_test_view(test_object=acl)
self.add_test_view(test_object=self._test_case_acl)
context = self.get_test_view()
resolved_link = link_acl_delete.resolve(context=context)
self.assertNotEqual(resolved_link, None)
self.assertEqual(
resolved_link.url, reverse('acls:acl_delete', args=(acl.pk,))
resolved_link.url, reverse(
viewname='acls:acl_delete',
kwargs={'acl_id': self._test_case_acl.pk}
)
)
def test_document_acl_edit_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
def test_object_acl_edit_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
self.add_test_view(test_object=acl)
self.add_test_view(test_object=self._test_case_acl)
context = self.get_test_view()
resolved_link = link_acl_permissions.resolve(context=context)
self.assertNotEqual(resolved_link, None)
self.assertEqual(
resolved_link.url, reverse('acls:acl_permissions', args=(acl.pk,))
resolved_link.url, reverse(
viewname='acls:acl_permissions',
kwargs={'acl_id': self._test_case_acl.pk}
)
)
def test_document_acl_list_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
def test_object_acl_list_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
acl.permissions.add(permission_acl_view.stored_permission)
self.login_user()
self.add_test_view(test_object=self.document)
self.add_test_view(test_object=self.test_object)
context = self.get_test_view()
resolved_link = link_acl_list.resolve(context=context)
self.assertNotEqual(resolved_link, None)
content_type = ContentType.objects.get_for_model(self.document)
kwargs = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.document.pk
}
self.assertEqual(
resolved_link.url, reverse('acls:acl_list', kwargs=kwargs)
resolved_link.url, reverse(
viewname='acls:acl_list',
kwargs=self.test_content_object_view_kwargs
)
)

View File

@@ -1,159 +1,401 @@
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import PermissionDenied
from django.db import models
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import (
TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL,
TEST_SMALL_DOCUMENT_PATH
DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
)
from ..classes import ModelPermission
from ..models import AccessControlList
from .mixins import ACLTestMixin
class PermissionTestCase(DocumentTestMixin, BaseTestCase):
auto_create_document_type = False
class PermissionTestCase(BaseTestCase):
def setUp(self):
super(PermissionTestCase, self).setUp()
self.document_type_1 = DocumentType.objects.create(
self.test_document_type_1 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_LABEL
)
self.document_type_2 = DocumentType.objects.create(
self.test_document_type_2 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_2_LABEL
)
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document_1 = self.document_type_1.new_document(
file_object=file_object
)
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document_2 = self.document_type_1.new_document(
file_object=file_object
)
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document_3 = self.document_type_2.new_document(
file_object=file_object
)
def tearDown(self):
for document_type in DocumentType.objects.all():
document_type.delete()
super(PermissionTestCase, self).tearDown()
self.test_document_1 = self.upload_document(
document_type=self.test_document_type_1
)
self.test_document_2 = self.upload_document(
document_type=self.test_document_type_1
)
self.test_document_3 = self.upload_document(
document_type=self.test_document_type_2
)
def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied):
AccessControlList.objects.check_access(
permissions=(permission_document_view,),
user=self.user, obj=self.document_1
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
)
def test_filtering_without_permissions(self):
self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
), []
self.assertEqual(
AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self._test_case_user,
).count(), 0
)
def test_check_access_with_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_1, role=self.role
content_object=self.test_document_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_permissions(self):
acl = AccessControlList.objects.create(
content_object=self.document_1, role=self.role
content_object=self.test_document_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
), (repr(self.document_1),)
AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self._test_case_user
), (repr(self.test_document_1),)
)
def test_check_access_with_inherited_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_check_access_with_inherited_acl_and_local_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
def test_check_access_with_inherited_acl_and_direct_acl(self):
test_acl_1 = AccessControlList.objects.create(
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
test_acl_1.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
test_acl_2 = AccessControlList.objects.create(
content_object=self.test_document_3, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
test_acl_2.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_3
obj=self.test_document_3, permission=permission_document_view,
user=self._test_case_user
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_inherited_permissions(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=Document.objects.all(),
user=self._test_case_user
)
# Since document_1 and document_2 are of document_type_1
# they are the only ones that should be returned
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 not in result)
self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.test_document_3 not in result)
def test_filtering_with_inherited_permissions_and_local_acl(self):
self.role.permissions.add(permission_document_view.stored_permission)
self._test_case_role.permissions.add(
permission_document_view.stored_permission
)
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
content_object=self.test_document_3, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=Document.objects.all(),
user=self._test_case_user,
)
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 in result)
self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.test_document_3 in result)
class InheritedPermissionTestCase(ACLTestMixin, BaseTestCase):
def test_retrieve_inherited_role_permission_not_model_applicable(self):
self._create_test_model()
self.test_object = self.TestModel.objects.create()
self._create_test_acl()
self._create_test_permission()
self.test_role.grant(permission=self.test_permission)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=self.test_object, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission not in queryset)
def test_retrieve_inherited_role_permission_model_applicable(self):
self._create_test_model()
self.test_object = self.TestModel.objects.create()
self._create_test_acl()
self._create_test_permission()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
self.test_permission,
)
)
self.test_role.grant(permission=self.test_permission)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=self.test_object, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission in queryset)
def test_retrieve_inherited_related_parent_child_permission(self):
self._create_test_permission()
self._create_test_model(model_name='TestModelParent')
self._create_test_model(
fields={
'parent': models.ForeignKey(
on_delete=models.CASCADE, related_name='children',
to='TestModelParent',
)
}, model_name='TestModelChild'
)
ModelPermission.register(
model=self.TestModelParent, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelChild, permissions=(
self.test_permission,
)
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent',
)
parent = self.TestModelParent.objects.create()
child = self.TestModelChild.objects.create(parent=parent)
AccessControlList.objects.grant(
obj=parent, permission=self.test_permission, role=self.test_role
)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=child, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission in queryset)
def test_retrieve_inherited_related_grandparent_parent_child_permission(self):
self._create_test_permission()
self._create_test_model(model_name='TestModelGrandParent')
self._create_test_model(
fields={
'parent': models.ForeignKey(
on_delete=models.CASCADE, related_name='children',
to='TestModelGrandParent',
)
}, model_name='TestModelParent'
)
self._create_test_model(
fields={
'parent': models.ForeignKey(
on_delete=models.CASCADE, related_name='children',
to='TestModelParent',
)
}, model_name='TestModelChild'
)
ModelPermission.register(
model=self.TestModelGrandParent, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelParent, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelChild, permissions=(
self.test_permission,
)
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent',
)
ModelPermission.register_inheritance(
model=self.TestModelParent, related='parent',
)
grandparent = self.TestModelGrandParent.objects.create()
parent = self.TestModelParent.objects.create(parent=grandparent)
child = self.TestModelChild.objects.create(parent=parent)
AccessControlList.objects.grant(
obj=grandparent, permission=self.test_permission,
role=self.test_role
)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=child, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission in queryset)
class MultipleAccessTestCase(ACLTestMixin, BaseTestCase):
def setUp(self):
super(MultipleAccessTestCase, self).setUp()
self._create_test_permission()
self._create_test_permission_2()
self._create_test_model(model_name='TestModelParent1')
self._create_test_model(model_name='TestModelParent2')
self._create_test_model(
fields={
'parent_1': models.ForeignKey(
on_delete=models.CASCADE, related_name='children1',
to='TestModelParent1',
),
'parent_2': models.ForeignKey(
on_delete=models.CASCADE, related_name='children2',
to='TestModelParent2',
)
}, model_name='TestModelChild'
)
ModelPermission.register(
model=self.TestModelParent1, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelParent2, permissions=(
self.test_permission_2,
)
)
self.test_object_parent_1 = self.TestModelParent1.objects.create()
self.test_object_parent_2 = self.TestModelParent2.objects.create()
self.test_object_child = self.TestModelChild.objects.create(
parent_1=self.test_object_parent_1, parent_2=self.test_object_parent_2
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent_1'
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent_2'
)
def test_restrict_queryset_and_operator_first_permission(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_AND,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child not in queryset)
def test_restrict_queryset_and_operator_second_permission(self):
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_AND,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child not in queryset)
def test_restrict_queryset_and_operator_both_permissions(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_AND,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)
def test_restrict_queryset_or_operator_first_permission(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_OR,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)
def test_restrict_queryset_or_operator_second_permission(self):
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_OR,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)
def test_restrict_queryset_or_operator_both_permissions(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_OR,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)

View File

@@ -1,191 +1,239 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from ..classes import ModelPermission
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class AccessControlListViewTestCase(GenericDocumentViewTestCase):
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
def setUp(self):
super(AccessControlListViewTestCase, self).setUp()
content_type = ContentType.objects.get_for_model(self.document)
self._create_test_model()
self._create_test_object()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
permission_acl_edit, permission_acl_view,
)
)
self.view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.document.pk
}
self._create_test_permission()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
self.test_permission,
)
)
def test_acl_create_view_no_permission(self):
self.login_user()
self._inject_test_object_content_type()
response = self.get(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
self._create_test_acl()
self.test_acl.permissions.add(self.test_permission.stored_permission)
def _request_acl_create_get_view(self):
return self.get(
viewname='acls:acl_create',
kwargs=self.test_content_object_view_kwargs, data={
'role': self.test_role.pk
}
)
self.assertEquals(response.status_code, 403)
self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_get_view_no_permission(self):
self.test_acl.delete()
def test_acl_create_view_with_permission(self):
self.login_user()
response = self._request_acl_create_get_view()
self.assertEqual(response.status_code, 404)
self.role.permissions.add(
permission_acl_edit.stored_permission
)
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
response = self.get(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
def test_acl_create_get_view_with_object_access(self):
self.test_acl.delete()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_acl_create_get_view()
self.assertContains(
response, text=self.document.label, status_code=200
response=response, text=force_text(self.test_object),
status_code=200
)
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def _request_acl_create_post_view(self):
return self.post(
viewname='acls:acl_create',
kwargs=self.test_content_object_view_kwargs, data={
'role': self.test_role.pk
}
)
def test_acl_create_view_post_no_permission(self):
self.login_user()
self.test_acl.delete()
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}
)
response = self._request_acl_create_post_view()
self.assertEqual(response.status_code, 404)
self.assertEquals(response.status_code, 403)
self.assertEqual(AccessControlList.objects.count(), 0)
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_create_view_with_post_permission(self):
self.login_user()
def test_acl_create_view_post_with_access(self):
self.test_acl.delete()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
self.role.permissions.add(
permission_acl_edit.stored_permission
)
response = self._request_acl_create_post_view()
self.assertEqual(response.status_code, 302)
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
self.assertContains(response, text='created', status_code=200)
self.assertEqual(AccessControlList.objects.count(), 1)
def test_acl_create_duplicate_view_with_permission(self):
def test_acl_create_duplicate_view_with_access(self):
"""
Test creating a duplicate ACL entry: same object & role
Result: Should redirect to existing ACL for object + role combination
"""
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_acl_create_post_view()
self.assertNotContains(
response=response, text=force_text(self.test_acl.role),
status_code=200
)
self.login_user()
# 2 ACLs: 1 created by the test and the other by the self.grant_access
self.assertEqual(AccessControlList.objects.count(), 2)
self.role.permissions.add(
permission_acl_edit.stored_permission
# Sorted by role PK
expected_results = sorted(
[
{
# Test role, created and then requested,
# but created only once
'object_id': self.test_object.pk,
'role': self.test_role.pk
},
{
# Test case ACL for the test case role, ignored
'object_id': self.test_object.pk,
'role': self._test_case_role.pk
},
], key=lambda item: item['role']
)
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
self.assertQuerysetEqual(
qs=AccessControlList.objects.order_by('role__id').values(
'object_id', 'role',
), transform=dict, values=expected_results
)
self.assertContains(
response, text='vailable permissions', status_code=200
)
self.assertEqual(AccessControlList.objects.count(), 1)
self.assertEqual(AccessControlList.objects.first().pk, acl.pk)
def test_orphan_acl_create_view_with_permission(self):
"""
Test creating an ACL entry for an object with no model permissions.
Result: Should display a blank permissions list (not optgroup)
"""
self.login_user()
self.role.permissions.add(
permission_acl_edit.stored_permission
def _request_acl_delete_view(self):
return self.post(
viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk}
)
recent_entry = self.document.add_as_recent_document_for_user(self.user)
content_type = ContentType.objects.get_for_model(recent_entry)
view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': recent_entry.pk
}
response = self.post(
viewname='acls:acl_create', kwargs=view_arguments, data={
'role': self.role.pk
}, follow=True
def test_acl_delete_view_no_permission(self):
response = self._request_acl_delete_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
self.assertNotContains(response, text='optgroup', status_code=200)
self.assertEqual(AccessControlList.objects.count(), 1)
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_delete_view_with_access(self):
self.grant_access(
obj=self.test_object, permission=permission_acl_edit
)
response = self._request_acl_delete_view()
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def _request_acl_list_view(self):
return self.get(
viewname='acls:acl_list', kwargs=self.test_content_object_view_kwargs
)
def test_acl_list_view_no_permission(self):
self.login_user()
response = self._request_acl_list_view()
document = self.document.add_as_recent_document_for_user(
self.user
).document
acl = AccessControlList.objects.create(
content_object=document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission)
content_type = ContentType.objects.get_for_model(document)
view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': document.pk
}
response = self.get(
viewname='acls:acl_list', kwargs=view_arguments
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
self.assertNotContains(response, text=document.label, status_code=403)
self.assertNotContains(response, text='otal: 1', status_code=403)
def test_acl_list_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
def test_acl_list_view_with_permission(self):
self.login_user()
response = self._request_acl_list_view()
self.role.permissions.add(
permission_acl_view.stored_permission
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
)
document = self.document.add_as_recent_document_for_user(
self.user
).document
acl = AccessControlList.objects.create(
content_object=document, role=self.role
def _request_get_acl_permissions_get_view(self):
return self.get(
viewname='acls:acl_permissions',
kwargs={'acl_id': self.test_acl.pk}
)
acl.permissions.add(permission_acl_view.stored_permission)
content_type = ContentType.objects.get_for_model(document)
def test_acl_permissions_get_view_no_permission(self):
self.test_acl.permissions.clear()
view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': document.pk
}
response = self.get(
viewname='acls:acl_list', kwargs=view_arguments
response = self._request_get_acl_permissions_get_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
self.assertFalse(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
def test_acl_permissions_get_view_with_access(self):
self.test_acl.permissions.clear()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_get_acl_permissions_get_view()
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
)
self.assertFalse(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
def _request_post_acl_permissions_post_view(self):
return self.post(
viewname='acls:acl_permissions',
kwargs={'acl_id': self.test_acl.pk},
data={'available-selection': self.test_permission.stored_permission.pk}
)
def test_acl_permissions_post_view_no_permission(self):
self.test_acl.permissions.clear()
response = self._request_post_acl_permissions_post_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
self.assertFalse(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
def test_acl_permissions_post_view_with_access(self):
self.test_acl.permissions.clear()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_post_acl_permissions_post_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
self.assertContains(response, text=document.label, status_code=200)

View File

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

View File

@@ -1,24 +1,23 @@
from __future__ import absolute_import, unicode_literals
import itertools
import logging
from django.contrib.contenttypes.models import ContentType
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.views import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
from mayan.apps.common.mixins import (
ContentTypeViewMixin, ExternalObjectMixin
)
from mayan.apps.common.generics import (
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView
)
from mayan.apps.permissions import Permission, PermissionNamespace
from mayan.apps.permissions.models import StoredPermission
from mayan.apps.permissions.models import Role
from .classes import ModelPermission
from .forms import ACLCreateForm
from .icons import icon_acl_list
from .links import link_acl_create
from .models import AccessControlList
@@ -27,113 +26,95 @@ from .permissions import permission_acl_edit, permission_acl_view
logger = logging.getLogger(__name__)
class ACLCreateView(SingleObjectCreateView):
fields = ('role',)
model = AccessControlList
class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView):
content_type_url_kw_args = {
'app_label': 'app_label',
'model': 'model_name'
}
external_object_permission = permission_acl_edit
external_object_pk_url_kwarg = 'object_id'
form_class = ACLCreateForm
def dispatch(self, request, *args, **kwargs):
self.object_content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
def get_error_message_duplicate(self):
return _(
'An ACL for "%(object)s" using role "%(role)s" already exists. '
'Edit that ACL entry instead.'
) % {'object': self.get_external_object(), 'role': self.object.role}
try:
self.content_object = self.object_content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except self.object_content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=self.content_object
)
return super(ACLCreateView, self).dispatch(request, *args, **kwargs)
def get_instance_extra_data(self):
return {
'content_object': self.content_object
}
def form_valid(self, form):
try:
acl = AccessControlList.objects.get(
content_type=self.object_content_type,
object_id=self.content_object.pk,
role=form.cleaned_data['role']
)
except AccessControlList.DoesNotExist:
return super(ACLCreateView, self).form_valid(form)
else:
return HttpResponseRedirect(
reverse('acls:acl_permissions', args=(acl.pk,))
)
def get_external_object_queryset(self):
# Here we get a queryset the object model for which an ACL will be
# created.
return self.get_content_type().get_all_objects_for_this_type()
def get_extra_context(self):
return {
'object': self.content_object,
'object': self.get_external_object(),
'title': _(
'New access control lists for: %s'
) % self.content_object
) % self.get_external_object()
}
def get_form_extra_kwargs(self):
return {
'field_name': 'role',
'label': _('Role'),
'queryset': Role.objects.exclude(
pk__in=self.get_external_object().acls.values('role')
),
'widget_attributes': {'class': 'select2'},
'user': self.request.user
}
def get_instance_extra_data(self):
return {
'content_object': self.get_external_object()
}
def get_queryset(self):
self.get_external_object().acls.all()
def get_success_url(self):
if self.object.pk:
return reverse('acls:acl_permissions', args=(self.object.pk,))
else:
return super(ACLCreateView, self).get_success_url()
return self.object.get_absolute_url()
class ACLDeleteView(SingleObjectDeleteView):
model = AccessControlList
def dispatch(self, request, *args, **kwargs):
acl = get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=acl.content_object
)
return super(ACLDeleteView, self).dispatch(request, *args, **kwargs)
object_permission = permission_acl_edit
pk_url_kwarg = 'acl_id'
def get_extra_context(self):
acl = self.get_object()
return {
'object': self.get_object().content_object,
'acl': acl,
'object': acl.content_object,
'navigation_object_list': ('object', 'acl'),
'title': _('Delete ACL: %s') % self.get_object(),
}
def get_post_action_redirect(self):
instance = self.get_object()
return reverse(
'acls:acl_list', args=(
instance.content_type.app_label,
instance.content_type.model, instance.object_id
)
'acls:acl_list', kwargs={
'app_label': instance.content_type.app_label,
'model_name': instance.content_type.model,
'object_id': instance.object_id
}
)
class ACLListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
self.object_content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
content_type_url_kw_args = {
'app_label': 'app_label',
'model': 'model_name'
}
external_object_permission = permission_acl_view
external_object_pk_url_kwarg = 'object_id'
try:
self.content_object = self.object_content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except self.object_content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=request.user,
obj=self.content_object
)
return super(ACLListView, self).dispatch(request, *args, **kwargs)
def get_external_object_queryset(self):
# Here we get a queryset the object model for which an ACL will be
# created.
return self.get_content_type().get_all_objects_for_this_type()
def get_extra_context(self):
return {
@@ -141,7 +122,9 @@ class ACLListView(SingleObjectListView):
'no_results_icon': icon_acl_list,
'no_results_main_link': link_acl_create.resolve(
context=RequestContext(
self.request, {'resolved_object': self.content_object}
self.request, {
'resolved_object': self.get_external_object()
}
)
),
'no_results_title': _(
@@ -149,116 +132,98 @@ class ACLListView(SingleObjectListView):
),
'no_results_text': _(
'ACL stands for Access Control List and is a precise method '
' to control user access to objects in the system.'
' to control user access to objects in the system. ACLs '
'allow granting a permission to a role but only for a '
'specific object or set of objects.'
),
'object': self.get_external_object(),
'title': _(
'Access control lists for: %s' % self.get_external_object()
),
'object': self.content_object,
'title': _('Access control lists for: %s' % self.content_object),
}
def get_object_list(self):
return AccessControlList.objects.filter(
content_type=self.object_content_type,
object_id=self.content_object.pk
def get_source_queryset(self):
return self.get_external_object().acls.all()
class ACLPermissionsView(AddRemoveView):
action_add_method = 'permissions_add'
action_remove_method = 'permissions_remove'
main_object_model = AccessControlList
main_object_permission = permission_acl_edit
main_object_pk_url_kwarg = 'acl_id'
list_added_title = _('Granted permissions')
list_available_title = _('Available permissions')
related_field = 'permissions'
def generate_choices(self, queryset):
namespaces_dictionary = {}
# Sort permissions by their translatable label
object_list = sorted(
queryset, key=lambda permission: permission.volatile_permission.label
)
class ACLPermissionsView(AssignRemoveView):
grouped = True
left_list_title = _('Available permissions')
right_list_title = _('Granted permissions')
@staticmethod
def generate_choices(entries):
results = []
entries = sorted(
entries, key=lambda x: (
x.volatile_permission.namespace.label,
x.volatile_permission.label
# Group permissions by namespace
for permission in object_list:
namespaces_dictionary.setdefault(
permission.volatile_permission.namespace.label,
[]
)
)
for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace):
permission_options = [
(force_text(permission.pk), permission) for permission in permissions
]
results.append(
(PermissionNamespace.get(name=namespace), permission_options)
namespaces_dictionary[permission.volatile_permission.namespace.label].append(
(permission.pk, force_text(permission))
)
return results
# Sort permissions by their translatable namespace label
return sorted(namespaces_dictionary.items())
def add(self, item):
permission = get_object_or_404(klass=StoredPermission, pk=item)
self.get_object().permissions.add(permission)
def dispatch(self, request, *args, **kwargs):
acl = get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=acl.content_object
)
return super(
ACLPermissionsView, self
).dispatch(request, *args, **kwargs)
def get_available_list(self):
return ModelPermission.get_for_instance(
instance=self.get_object().content_object
).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def get_disabled_choices(self):
"""
Get permissions from a parent's acls but remove the permissions we
already hold for this object
Get permissions from a parent's ACLs or directly granted to the role.
We return a list since that is what the form widget's can process.
"""
return map(
str, set(
self.get_object().get_inherited_permissions().values_list(
'pk', flat=True
)
).difference(
self.get_object().permissions.values_list('pk', flat=True)
)
)
return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
def get_extra_context(self):
return {
'object': self.get_object().content_object,
'title': _('Role "%(role)s" permission\'s for "%(object)s"') % {
'role': self.get_object().role,
'object': self.get_object().content_object,
},
'acl': self.main_object,
'object': self.main_object.content_object,
'navigation_object_list': ('object', 'acl'),
'title': _('Role "%(role)s" permission\'s for "%(object)s".') % {
'role': self.main_object.role,
'object': self.main_object.content_object,
}
}
def get_granted_list(self):
"""
Merge or permissions we hold for this object and the permissions we
hold for this object's parent via another ACL
"""
merged_pks = self.get_object().permissions.values_list('pk', flat=True) | self.get_object().get_inherited_permissions().values_list('pk', flat=True)
return StoredPermission.objects.filter(pk__in=merged_pks)
def get_object(self):
return get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
def get_right_list_help_text(self):
if self.get_object().get_inherited_permissions():
def get_list_added_help_text(self):
if self.main_object.get_inherited_permissions():
return _(
'Disabled permissions are inherited from a parent object.'
'Disabled permissions are inherited from a parent object or '
'directly granted to the role and can\'t be removed from this '
'view. Inherited permissions need to be removed from the '
'parent object\'s ACL or from them role via the Setup menu.'
)
return None
def get_list_added_queryset(self):
"""
Merge of permissions we hold for this object and the permissions we
hold for this object's parents via another ACL. .distinct() is added
in case the permission was added to the ACL and then added to a
parent ACL's and thus inherited and would appear twice. If
order to remove the double permission from the ACL it would need to be
remove from the parent first to enable the choice in the form,
remove it from the ACL and then re-add it to the parent ACL.
"""
queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
def left_list(self):
Permission.refresh()
return ACLPermissionsView.generate_choices(self.get_available_list())
return (
queryset_acl | self.main_object.get_inherited_permissions()
).distinct()
def remove(self, item):
permission = get_object_or_404(klass=StoredPermission, pk=item)
self.get_object().permissions.remove(permission)
def right_list(self):
return ACLPermissionsView.generate_choices(self.get_granted_list())
def get_secondary_object_source_queryset(self):
return ModelPermission.get_for_instance(
instance=self.main_object.content_object
)

View File

@@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction):
try:
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user, obj=obj
obj=obj, permissions=permission_acl_edit,
user=request.user
)
except Exception as exception:
raise ValidationError(exception)
@@ -98,7 +99,9 @@ class GrantAccessAction(WorkflowAction):
def get_form_schema(self, *args, **kwargs):
self.fields['content_type']['kwargs']['queryset'] = ModelPermission.get_classes(as_content_type=True)
self.fields['permissions']['kwargs']['choices'] = Permission.all(as_choices=True)
self.fields['permissions']['kwargs']['choices'] = Permission.all(
as_choices=True
)
return super(GrantAccessAction, self).get_form_schema(*args, **kwargs)
def get_execute_data(self):

View File

@@ -23,7 +23,7 @@ class FontAwesomeDriver(IconDriver):
self.symbol = symbol
def render(self):
return get_template(self.template_name).render(
return get_template(template_name=self.template_name).render(
context={'symbol': self.symbol}
)
@@ -37,7 +37,7 @@ class FontAwesomeDualDriver(IconDriver):
self.secondary_symbol = secondary_symbol
def render(self):
return get_template(self.template_name).render(
return get_template(template_name=self.template_name).render(
context={
'data': (
{
@@ -55,7 +55,6 @@ class FontAwesomeDualDriver(IconDriver):
)
class FontAwesomeCSSDriver(IconDriver):
name = 'fontawesomecss'
template_name = 'appearance/icons/font_awesome_css.html'
@@ -64,7 +63,7 @@ class FontAwesomeCSSDriver(IconDriver):
self.css_classes = css_classes
def render(self):
return get_template(self.template_name).render(
return get_template(template_name=self.template_name).render(
context={'css_classes': self.css_classes}
)
@@ -77,7 +76,7 @@ class FontAwesomeMasksDriver(IconDriver):
self.data = data
def render(self):
return get_template(self.template_name).render(
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
@@ -90,7 +89,7 @@ class FontAwesomeLayersDriver(IconDriver):
self.data = data
def render(self):
return get_template(self.template_name).render(
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)

View File

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

View File

@@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
namespace = Namespace(name='appearance', label=_('Appearance'))
namespace = Namespace(label=_('Appearance'), name='appearance')
setting_max_title_length = namespace.add_setting(
default=DEFAULT_MAXIMUM_TITLE_LENGTH,

View File

@@ -8,10 +8,10 @@ class MayanApp {
ajaxMenusOptions: []
}
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.ajaxExecuting = false;
this.ajaxMenusOptions = options.ajaxMenusOptions;
this.ajaxMenuHashes = {};
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.window = $(window);
}
@@ -29,29 +29,6 @@ class MayanApp {
}
}
static mayanNotificationBadge (options, data) {
// Callback to add the notifications count inside a badge markup
var notifications = data[options.attributeName];
if (notifications > 0) {
// Save the original link text before adding the initial badge markup
if (!options.element.data('mn-saved-text')) {
options.element.data('mn-saved-text', options.element.html());
}
options.element.html(
options.element.data('mn-saved-text') + ' <span class="badge">' + notifications + '</span>'
);
} else {
if (options.element.data('mn-saved-text')) {
// If there is a saved original link text, restore it
options.element.html(
options.element.data('mn-saved-text')
);
}
}
}
static setupMultiItemActions () {
$('body').on('change', '.check-all-slave', function () {
MayanApp.countChecked();
@@ -81,22 +58,6 @@ class MayanApp {
});
}
static tagSelectionTemplate (tag, container) {
var $tag = $(
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
);
container[0].style.background = tag.element.dataset.color;
return $tag;
}
static tagResultTemplate (tag) {
if (!tag.element) { return ''; }
var $tag = $(
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
);
return $tag;
}
static updateNavbarState () {
var uri = new URI(window.location.hash);
var uriFragment = uri.fragment();
@@ -110,35 +71,6 @@ class MayanApp {
// 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) {
// Convert the callback string to an actual function
var callbackFunction = window;
$.each(options.callback.split('.'), function (index, value) {
callbackFunction = callbackFunction[value]
});
callbackFunction(options, data);
} else {
options.element.text(data[options.attributeName]);
}
},
url: options.APIURL
});
}
callbackAJAXSpinnerUpdate () {
if (this.ajaxExecuting) {
$(this.ajaxSpinnerSeletor).fadeIn(50);
@@ -239,7 +171,6 @@ class MayanApp {
initialize () {
var self = this;
this.setupAJAXPeriodicWorkers();
this.setupAJAXSpinner();
this.setupFormHotkeys();
this.setupFullHeightResizing();
@@ -256,22 +187,6 @@ class MayanApp {
partialNavigation.initialize();
}
setupAJAXPeriodicWorkers () {
var app = this;
$('a[data-apw-url]').each(function() {
var $this = $(this);
app.AJAXperiodicWorker({
attributeName: $this.data('apw-attribute'),
APIURL: $this.data('apw-url'),
callback: $this.data('apw-callback'),
element: $this,
interval: $this.data('apw-interval'),
});
});
}
setupAJAXSpinner () {
var self = this;
@@ -445,12 +360,6 @@ class MayanApp {
dropdownAutoWidth: true,
width: '100%'
});
$('.select2-tags').select2({
templateSelection: MayanApp.tagSelectionTemplate,
templateResult: MayanApp.tagResultTemplate,
width: '100%'
});
}
resizeFullHeight () {

View File

@@ -37,7 +37,7 @@
</div>
</div>
{% get_menus_links names='facet,list facet' sort_results=True as links_facet %}
{% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}
<style>
@@ -45,12 +45,12 @@
<div class="row">
<div class="col-xs-12 {% if links_facet %}has-sidebar{% endif %}" id="viewport">
<div class="col-xs-12 {% if facet_menus_link_results %}has-sidebar{% endif %}" id="viewport">
{% include 'appearance/calculate_form_title.html' %}
{# action menu #}
{% get_menus_links names='object,sidebar,secondary' sort_results=True as links_actions %}
{% if links_actions %}
{% navigation_resolve_menus names='object,secondary' sort_results=True as action_menus_link_results %}
{% if action_menus_link_results %}
<div class="pull-right btn-group" id="menu-actions">
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
{% trans 'Actions' %}
@@ -58,19 +58,39 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links_actions %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% for menus_link_result in action_menus_link_results %}
{% if action_menus_link_results|length > 1 %}
<li class="dropdown-header">{{ menus_link_result.menu.label }}</li>
{% endif %}
{% if not forloop.last and object_navigation_links %}
{% for link_group in menus_link_result.link_groups %}
{% if navigation_object_list %}
{% ifchanged link_group.object %}
<li class="dropdown-header">{% common_get_object_verbose_name obj=link_group.object %}</li>
{% endifchanged %}
{% endif %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% if not forloop.last and link_group %}
<li class="divider"></li>
{% endif %}
{% endfor %}
{% if not forloop.last and menus_link_result %}
<li class="divider"></li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="clearfix"></div>
@@ -80,17 +100,21 @@
{% block footer %}{% endblock %}
</div>
{% if links_facet %}
{% if facet_menus_link_results %}
<div id="sidebar">
<div class="pull-right list-group">
{% for object_navigation_links in links_facet %}
{% with 'true' as hide_active_anchor %}
{% with 'active' as link_class_active %}
{% with 'list-group-item btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% for menu_link_result in facet_menus_link_results %}
{% for link_group in menu_link_result.link_groups %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as hide_active_anchor %}
{% with 'active' as link_class_active %}
{% with 'list-group-item btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</div>
</div>

View File

@@ -90,6 +90,17 @@
<i class="fa fa-times"></i> {% if cancel_label %}{{ cancel_label }}{% else %}{% trans 'Cancel' %}{% endif %}
</a>
{% endif %}
{% for button in extra_buttons %}
<button class="btn btn-default" name="{% if form.prefix %}{{ form.prefix }}-{{ button.name }}{% else %}{{ button.name }}{% endif %}" type="submit">
{% if button.icon_class %}
{{ button.icon_class.render }}
{% endif %}
{% if button.label %}{{ button.label }}{% else %}{% if object %}{% trans 'Save' %}{% else %}{% trans 'Submit' %}{% endif %}{% endif %}
</button>
{% endfor %}
</div>
{% endif %}
{% endif %}

View File

@@ -11,8 +11,10 @@
<div class="well center-block">
<div class="row">
{% with 'navigation/large_button_link.html' as link_template %}
{% for object_navigation_links in resolved_links %}
{% include 'navigation/generic_navigation.html' %}
{% for menu_results in resolved_links %}
{% with menu_results.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% empty %}
<p class="text-center">
{% include 'appearance/no_results.html' %}

View File

@@ -27,7 +27,7 @@
<div class="well center-block">
{% if object_list %}
{% if not hide_multi_item_actions %}
{% get_menu_links name='multi item' sort_results=True source=object_list.0 as links_multi_item %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% endif %}
{% endif %}
@@ -45,7 +45,7 @@
<div class="form-group">
<div class="checkbox">
<label for="id_indexes_0">
{% if links_multi_item %}
{% if links_multi_menus_results %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" />
{% endif %}
@@ -84,9 +84,9 @@
{% endfor %}
{% if not hide_links %}
{% get_menus_links names='list facet,object' source=object as links %}
{% navigation_resolve_menus names='list facet,object' source=object as facet_menus_link_results %}
{% if links %}
{% if facet_menus_link_results %}
<div class="dropdown text-center">
<button aria-expanded="false" aria-haspopup="true" class="btn btn-default btn-danger btn-sm dropdown-toggle" data-toggle="dropdown">
{% trans 'Actions' %}
@@ -94,18 +94,25 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% for facet_menu_link_result in facet_menus_link_results %}
{% for link_group in facet_menu_link_result.link_groups %}
{% if not forloop.last and object_navigation_links %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% if not forloop.last and facet_menu_link_result %}
<li class="divider"></li>
{% endif %}
{% endfor %}
</ul>
</div>

View File

@@ -27,7 +27,7 @@
<div class="well center-block">
{% if object_list %}
{% if not hide_multi_item_actions %}
{% get_menu_links name='multi item' sort_results=True source=object_list.0 as links_multi_item %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% endif %}
{% endif %}
@@ -42,21 +42,36 @@
<thead>
{% if not hide_header %}
<tr>
{% if links_multi_item %}
{% if links_multi_menus_results %}
<th class="first"></th>
{% endif %}
{% if not hide_object %}
<th>{% trans 'Identifier' %}</th>
{% else %}
{% get_source_columns source=object_list only_identifier=True as source_column %}
{% if source_column %}
<th>
{% if source_column.is_sortable %}
<a href="{% get_sort_field_querystring column=source_column %}">{{ source_column.label }}
{% if source_column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
{% else %}
{{ source_column.label }}
{% endif %}
</th>
{% endif %}
{% endif %}
{% if not hide_columns %}
{% get_source_columns source=object_list as source_columns %}
{% get_source_columns source=object_list exclude_identifier=True as source_columns %}
{% for column in source_columns %}
<th>
{% if column.is_sortable %}
<a href="{% get_sort_field_querystring column=column %}">{{ column.label }}
{% if column.attribute == sort_field %}
{% if column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
@@ -80,7 +95,7 @@
<tbody>
{% for object in object_list %}
<tr>
{% if links_multi_item %}
{% if links_multi_menus_results %}
<td>
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
</td>
@@ -100,7 +115,7 @@
</td>
{% endif %}
{% endif %}
{% if not hide_columns %}
{% if not hide_columns %}
{% get_source_columns source=object exclude_identifier=True as source_columns %}
{% for column in source_columns %}
<td>
@@ -116,21 +131,29 @@
{% endfor %}
{% if not hide_links %}
<td class="last">
{% get_menu_links name='list facet' sort_results=True source=object as resolved_links %}
{% for object_navigation_links in resolved_links %}
{% with 'true' as horizontal %}
{% with 'true' as hide_icon %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% navigation_resolve_menu name='list facet' sort_results=True source=object as facet_menus_results %}
{% for facet_menu_results in facet_menus_results %}
{% for link_group in facet_menu_results.link_groups %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as horizontal %}
{% with 'true' as hide_icon %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
{% get_menu_links name='object' source=object as resolved_links %}
{% for object_navigation_links in resolved_links %}
{% with 'true' as horizontal %}
{% with 'true' as hide_icon %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% navigation_resolve_menu name='object' source=object as object_menus_results %}
{% for object_menu_results in object_menus_results %}
{% for link_group in object_menu_results.link_groups %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as horizontal %}
{% with 'true' as hide_icon %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</td>
{% endif %}

View File

@@ -6,7 +6,7 @@
<div class="pull-left">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group">
{% if links_multi_item %}
{% if links_multi_menus_results %}
<a class="btn btn-default btn-sm check-all" data-checked=false data-icon-checked="fa fa-check-square" data-icon-unchecked="far fa-square" title="{% trans 'Select/Deselect all' %}">
<i class="far fa-square"></i>
</a>
@@ -19,7 +19,7 @@
</div>
{% if links_multi_item %}
{% if links_multi_menus_results %}
<p class="pull-right" id="multi-item-title" style="margin-top: 4px;">{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}</p>
<div class="pull-right btn-group" id="multi-item-actions" style="display: none;">
@@ -29,16 +29,20 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links_multi_item %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm btn-multi-item-action' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% for multi_item_menu_results in links_multi_menus_results %}
{% for link_group in multi_item_menu_results.link_groups %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm btn-multi-item-action' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% if not forloop.last and object_navigation_links %}
{% endfor %}
{% if not forloop.last and link_group %}
<li class="divider"></li>
{% endif %}
{% endfor %}

View File

@@ -8,58 +8,62 @@
{% spaceless %}
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
{% get_menu_links name='main' as menu_links %}
{% for link_set in menu_links %}
{% for link in link_set %}
{% with 'active' as li_class_active %}
{% with ' ' as link_classes %}
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
<div class="pull-left">
{% if link.icon %}
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
{% endif %}
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
{{ link.label }}
</div>
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
<div class="clearfix"></div>
</a>
</h4>
</div>
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<ul class="list-unstyled">
{% get_menu_links name=link.name as menu_links %}
{% for linkset in menu_links %}
{% with '' as link_class_active %}
{% with 'a-main-menu-accordion-link' as link_classes %}
{% with 'true' as as_li %}
{% with linkset as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
</ul>
{% navigation_resolve_menu name='main' as main_menus_results %}
{% for main_menu_results in main_menus_results %}
{% for link_group in main_menu_results.link_groups %}
{% for link in link_group.links %}
{% with 'active' as li_class_active %}
{% with ' ' as link_classes %}
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
<div class="pull-left">
{% if link.icon %}
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
{% endif %}
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
{{ link.label }}
</div>
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
<div class="clearfix"></div>
</a>
</h4>
</div>
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<ul class="list-unstyled">
{% navigation_resolve_menu name=link.name as sub_menus_results %}
{% for sub_menu_results in sub_menus_results %}
{% for link_group in sub_menu_results.link_groups %}
{% with '' as link_class_active %}
{% with 'a-main-menu-accordion-link' as link_classes %}
{% with 'true' as as_li %}
{% with link_group.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
{% else %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
{% include 'navigation/generic_link_instance.html' %}
</h4>
{% else %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
{% include 'navigation/generic_link_instance.html' %}
</h4>
</div>
</div>
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
{% endfor %}
</div>

View File

@@ -18,20 +18,22 @@
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
{% get_menu_links name='topbar' as menu_links %}
{% for link_set in menu_links %}
{% for link in link_set %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'active' as li_class_active %}
{% with 'first' as li_class_first %}
{% with ' ' as link_classes %}
{% include 'navigation/generic_subnavigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
{% for tobpar_menu_result in topbar_menus_results %}
{% for link_group in tobpar_menu_result.link_groups %}
{% for link in link_group.links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'active' as li_class_active %}
{% with 'first' as li_class_first %}
{% with ' ' as link_classes %}
{% include 'navigation/generic_subnavigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
{% endfor %}
</ul>

View File

@@ -12,7 +12,11 @@ def get_choice_value(field):
try:
return dict(field.field.choices)[field.value()]
except TypeError:
return ', '.join([subwidget.data['label'] for subwidget in field.subwidgets if subwidget.data['selected']])
return ', '.join(
[
subwidget.data['label'] for subwidget in field.subwidgets if subwidget.data['selected']
]
)
except KeyError:
return _('None')
@@ -24,4 +28,4 @@ def get_form_media_js(form):
@register.simple_tag
def get_icon(icon_path):
return import_string(icon_path).render()
return import_string(dotted_path=icon_path).render()

View File

@@ -23,8 +23,10 @@ class EmailAuthenticationForm(forms.Form):
remember_me = forms.BooleanField(label=_('Remember me'), required=False)
error_messages = {
'invalid_login': _('Please enter a correct email and password. '
'Note that the password field is case-sensitive.'),
'invalid_login': _(
'Please enter a correct email and password. Note that the '
'password field is case-sensitive.'
),
'inactive': _('This account is inactive.'),
}
@@ -56,8 +58,10 @@ class EmailAuthenticationForm(forms.Form):
return self.cleaned_data
def check_for_test_cookie(self):
warnings.warn('check_for_test_cookie is deprecated; ensure your login '
'view is CSRF-protected.', DeprecationWarning)
warnings.warn(
'check_for_test_cookie is deprecated; ensure your login view '
'is CSRF-protected.', DeprecationWarning
)
def get_user_id(self):
if self.user_cache:

View File

@@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_LOGIN_METHOD, DEFAULT_MAXIMUM_SESSION_LENGTH
namespace = Namespace(name='authentication', label=_('Authentication'))
namespace = Namespace(label=_('Authentication'), name='authentication')
setting_login_method = namespace.add_setting(
global_name='AUTHENTICATION_LOGIN_METHOD', default=DEFAULT_LOGIN_METHOD,
help_text=_(

View File

@@ -14,8 +14,7 @@
<div class="col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
<div class="alert alert-success" role="alert">{% trans 'Password reset complete! Click the link below to login.' %}</div>
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:logout_view' %}">{% trans 'Login page' %}</a></div>
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:login_view' %}">{% trans 'Login page' %}</a></div>
</div>
</div>
{% endblock content_plain %}

View File

@@ -8,8 +8,7 @@ from django.urls import reverse
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.tests.literals import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_USER_PASSWORD_EDITED
TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME,
)
from ..settings import setting_maximum_session_length
@@ -19,97 +18,101 @@ from .literals import TEST_EMAIL_AUTHENTICATION_BACKEND
class UserLoginTestCase(GenericViewTestCase):
"""
Test that users can login via the supported authentication methods
Test that users can login using the supported authentication methods
"""
authenticated_url = '{}?next={}'.format(
reverse(settings.LOGIN_URL), reverse(viewname='documents:document_list')
)
auto_login_user = False
def setUp(self):
super(UserLoginTestCase, self).setUp()
Namespace.invalidate_cache_all()
def _request_authenticated_view(self):
return self.get(viewname='documents:document_list')
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_normal_behavior(self):
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
self.assertRedirects(
response,
'http://testserver/authentication/login/?next=/documents/list/'
response=response, expected_url=self.authenticated_url
)
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_login(self):
logged_in = self.client.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
logged_in = self.login(
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
)
self.assertTrue(logged_in)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='email')
def test_email_login(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
logged_in = self.client.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
logged_in = self.login(
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
)
self.assertFalse(logged_in)
logged_in = self.client.login(
email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD
logged_in = self.login(
email=TEST_CASE_USER_EMAIL, password=TEST_CASE_USER_PASSWORD
)
self.assertTrue(logged_in)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_login_via_views(self):
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
self.assertRedirects(
response,
'http://testserver/authentication/login/?next=/documents/list/'
response=response, expected_url=self.authenticated_url
)
response = self.client.post(
reverse(settings.LOGIN_URL), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD
response = self.post(
viewname=settings.LOGIN_URL, data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD
}
)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='email')
def test_email_login_via_views(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
self.assertRedirects(
response,
'http://testserver/authentication/login/?next=/documents/list/'
response=response, expected_url=self.authenticated_url
)
response = self.client.post(
reverse(settings.LOGIN_URL), {
'email': TEST_ADMIN_EMAIL, 'password': TEST_ADMIN_PASSWORD
response = self.post(
viewname=settings.LOGIN_URL, data={
'email': TEST_CASE_USER_EMAIL, 'password': TEST_CASE_USER_PASSWORD
}, follow=True
)
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_remember_me(self):
response = self.client.post(
reverse(settings.LOGIN_URL), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD,
response = self.post(
viewname=settings.LOGIN_URL, data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD,
'remember_me': True
}, follow=True
)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
self.assertEqual(response.status_code, 200)
self.assertEqual(
@@ -120,15 +123,15 @@ class UserLoginTestCase(GenericViewTestCase):
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_dont_remember_me(self):
response = self.client.post(
reverse(settings.LOGIN_URL), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD,
response = self.post(
viewname=settings.LOGIN_URL, data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD,
'remember_me': False
}, follow=True
)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
self.assertEqual(response.status_code, 200)
self.assertTrue(self.client.session.get_expire_at_browser_close())
@@ -136,15 +139,15 @@ class UserLoginTestCase(GenericViewTestCase):
@override_settings(AUTHENTICATION_LOGIN_METHOD='email')
def test_email_remember_me(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.client.post(
reverse(settings.LOGIN_URL), {
'email': TEST_ADMIN_EMAIL,
'password': TEST_ADMIN_PASSWORD,
response = self.post(
viewname=settings.LOGIN_URL, data={
'email': TEST_CASE_USER_EMAIL,
'password': TEST_CASE_USER_PASSWORD,
'remember_me': True
}, follow=True
)
response = self.client.get(reverse('documents:document_list'))
response = self._request_authenticated_view()
self.assertEqual(response.status_code, 200)
self.assertEqual(
@@ -158,13 +161,13 @@ class UserLoginTestCase(GenericViewTestCase):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.post(
viewname=settings.LOGIN_URL, data={
'email': TEST_ADMIN_EMAIL,
'password': TEST_ADMIN_PASSWORD,
'email': TEST_CASE_USER_EMAIL,
'password': TEST_CASE_USER_PASSWORD,
'remember_me': False
}
)
response = self.get(viewname='documents:document_list')
response = self._request_authenticated_view()
self.assertEqual(response.status_code, 200)
self.assertTrue(self.client.session.get_expire_at_browser_close())
@@ -173,7 +176,7 @@ class UserLoginTestCase(GenericViewTestCase):
def test_password_reset(self):
response = self.post(
viewname='authentication:password_reset_view', data={
'email': TEST_ADMIN_EMAIL,
'email': TEST_CASE_USER_EMAIL,
}
)
@@ -185,29 +188,29 @@ class UserLoginTestCase(GenericViewTestCase):
response = self.post(
viewname='authentication:password_reset_confirm_view',
args=uid_token[-3:-1], data={
'new_password1': TEST_USER_PASSWORD_EDITED,
'new_password2': TEST_USER_PASSWORD_EDITED,
'new_password1': TEST_CASE_USER_PASSWORD,
'new_password2': TEST_CASE_USER_PASSWORD,
}
)
self.assertEqual(response.status_code, 302)
self.login(
username=TEST_ADMIN_USERNAME, password=TEST_USER_PASSWORD_EDITED
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
)
response = self.get(viewname='documents:document_list')
response = self._request_authenticated_view()
self.assertEqual(response.status_code, 200)
def test_username_login_redirect(self):
TEST_REDIRECT_URL = reverse('common:about_view')
TEST_REDIRECT_URL = reverse(viewname='common:about_view')
response = self.client.post(
'{}?next={}'.format(
response = self.post(
path='{}?next={}'.format(
reverse(settings.LOGIN_URL), TEST_REDIRECT_URL
), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD,
), data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD,
'remember_me': False
}, follow=True
)

View File

@@ -1,42 +1,44 @@
from __future__ import unicode_literals
from django.conf import settings
from django.conf.urls import url
from django.contrib.auth.views import logout
from .views import (
login_view, password_change_done, password_change_view,
password_reset_complete_view, password_reset_confirm_view,
password_reset_done_view, password_reset_view
MayanLoginView, MayanLogoutView, MayanPasswordChangeDoneView,
MayanPasswordChangeView, MayanPasswordResetCompleteView,
MayanPasswordResetConfirmView, MayanPasswordResetDoneView,
MayanPasswordResetView
)
urlpatterns = [
url(r'^login/$', login_view, name='login_view'),
url(regex=r'^login/$', name='login_view', view=MayanLoginView.as_view()),
url(
r'^password/change/done/$', password_change_done,
name='password_change_done'
regex=r'^logout/$', name='logout_view', view=MayanLogoutView.as_view()
),
url(
r'^password/change/$', password_change_view,
name='password_change_view'
regex=r'^password/change/$', name='password_change_view',
view=MayanPasswordChangeView.as_view()
),
url(
r'^logout/$', logout, {'next_page': settings.LOGIN_REDIRECT_URL},
name='logout_view'
regex=r'^password/change/done/$', name='password_change_done',
view=MayanPasswordChangeDoneView.as_view()
),
url(
r'^password/reset/$', password_reset_view, name='password_reset_view'
regex=r'^password/reset/$', name='password_reset_view',
view=MayanPasswordResetView.as_view()
),
url(
r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
password_reset_confirm_view, name='password_reset_confirm_view'
regex=r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
name='password_reset_confirm_view',
view=MayanPasswordResetConfirmView.as_view()
),
url(
r'^password/reset/complete/$', password_reset_complete_view,
name='password_reset_complete_view'
regex=r'^password/reset/complete/$',
name='password_reset_complete_view',
view=MayanPasswordResetCompleteView.as_view()
),
url(
r'^password/reset/done/$', password_reset_done_view,
name='password_reset_done_view'
regex=r'^password/reset/done/$', name='password_reset_done_view',
view=MayanPasswordResetDoneView.as_view()
),
]

View File

@@ -1,19 +1,17 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import (
login, password_change, password_reset, password_reset_complete,
password_reset_confirm, password_reset_done
LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView,
PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView,
PasswordResetView
)
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, resolve_url
from django.urls import reverse
from django.utils.http import is_safe_url
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from stronghold.decorators import public
from stronghold.views import StrongholdPublicMixin
import mayan
from mayan.apps.common.settings import (
@@ -24,143 +22,108 @@ from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length
@public
def login_view(request):
"""
Control how the use is to be authenticated, options are 'email' and
'username'
"""
success_url_allowed_hosts = set()
kwargs = {'template_name': 'authentication/login.html'}
class MayanLoginView(StrongholdPublicMixin, LoginView):
extra_context = {
'appearance_type': 'plain'
}
template_name = 'authentication/login.html'
redirect_authenticated_user = True
if setting_login_method.value == 'email':
kwargs['authentication_form'] = EmailAuthenticationForm
else:
kwargs['authentication_form'] = UsernameAuthenticationForm
def form_valid(self, form):
result = super(MayanLoginView, self).form_valid(form=form)
remember_me = form.cleaned_data.get('remember_me')
allowed_hosts = {request.get_host()}
allowed_hosts.update(success_url_allowed_hosts)
# remember_me values:
# True - long session
# False - short session
# None - Form has no remember_me value and we let the session
# expiration default.
redirect_to = request.POST.get(
REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '')
)
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts=allowed_hosts,
require_https=request.is_secure(),
)
url = redirect_to if url_is_safe else ''
if not request.user.is_authenticated:
extra_context = {
'appearance_type': 'plain',
REDIRECT_FIELD_NAME: url or resolve_url(settings.LOGIN_REDIRECT_URL)
}
result = login(request, extra_context=extra_context, **kwargs)
if request.method == 'POST':
form = kwargs['authentication_form'](request, data=request.POST)
if form.is_valid():
if form.cleaned_data['remember_me']:
request.session.set_expiry(
setting_maximum_session_length.value
)
else:
request.session.set_expiry(0)
return result
else:
return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL))
def password_change_view(request):
"""
Password change wrapper for better control
"""
extra_context = {'title': _('Current user password change')}
if request.user.user_options.block_password_change:
messages.error(
request, _(
'Changing the password is not allowed for this account.'
if remember_me is True:
self.request.session.set_expiry(
setting_maximum_session_length.value
)
elif remember_me is False:
self.request.session.set_expiry(0)
return result
def get_form_class(self):
if setting_login_method.value == 'email':
return EmailAuthenticationForm
else:
return UsernameAuthenticationForm
class MayanLogoutView(LogoutView):
"""No current change or overrides, left here for future expansion"""
class MayanPasswordChangeDoneView(PasswordChangeDoneView):
def dispatch(self, *args, **kwargs):
messages.success(
message=_('Your password has been successfully changed.'),
request=self.request
)
return HttpResponseRedirect(reverse(setting_home_view.view))
return password_change(
request, extra_context=extra_context,
template_name='appearance/generic_form.html',
post_change_redirect=reverse('authentication:password_change_done'),
)
return redirect(to='common:current_user_details')
def password_change_done(request):
"""
View called when the new user password has been accepted
"""
messages.success(
request, _('Your password has been successfully changed.')
)
return redirect('common:current_user_details')
class MayanPasswordChangeView(PasswordChangeView):
extra_context = {'title': _('Current user password change')}
success_url = reverse_lazy(viewname='authentication:password_change_done')
template_name = 'appearance/generic_form.html'
def dispatch(self, *args, **kwargs):
if self.request.user.user_options.block_password_change:
messages.error(
message=_(
'Changing the password is not allowed for this account.'
), request=self.request
)
return HttpResponseRedirect(
redirect_to=reverse(viewname=setting_home_view.view)
)
return super(MayanPasswordChangeView, self).dispatch(*args, **kwargs)
@public
def password_reset_complete_view(request):
class MayanPasswordResetCompleteView(StrongholdPublicMixin, PasswordResetCompleteView):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_complete(
request, extra_context=extra_context,
template_name='authentication/password_reset_complete.html'
)
template_name = 'authentication/password_reset_complete.html'
@public
def password_reset_confirm_view(request, uidb64=None, token=None):
class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmView):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_confirm(
request, extra_context=extra_context,
template_name='authentication/password_reset_confirm.html',
post_reset_redirect=reverse(
'authentication:password_reset_complete_view'
), uidb64=uidb64, token=token
success_url = reverse_lazy(
viewname='authentication:password_reset_complete_view'
)
template_name = 'authentication/password_reset_confirm.html'
@public
def password_reset_done_view(request):
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_done(
request, extra_context=extra_context,
template_name='authentication/password_reset_done.html'
)
template_name = 'authentication/password_reset_done.html'
@public
def password_reset_view(request):
class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
email_template_name = 'authentication/password_reset_email.html'
extra_context = {
'appearance_type': 'plain'
}
return password_reset(
request, extra_context=extra_context,
email_template_name='authentication/password_reset_email.html',
extra_email_context={
'project_title': setting_project_title.value,
'project_website': setting_project_url.value,
'project_copyright': mayan.__copyright__,
'project_license': mayan.__license__,
}, subject_template_name='authentication/password_reset_subject.txt',
template_name='authentication/password_reset_form.html',
post_reset_redirect=reverse(
'authentication:password_reset_done_view'
)
extra_email_context = {
'project_copyright': mayan.__copyright__,
'project_license': mayan.__license__,
'project_title': setting_project_title.value,
'project_website': setting_project_url.value
}
subject_template_name = 'authentication/password_reset_subject.txt'
success_url = reverse_lazy(
viewname='authentication:password_reset_done_view'
)
template_name = 'authentication/password_reset_form.html'

View File

@@ -40,16 +40,18 @@ class AutoadminAccountAdapter(DefaultAccountAdapter):
Give superuser privileges automagically if the email address of a
user confirming their email is listed in ``settings.ADMINS``.
"""
super(AutoadminAccountAdapter,
self).confirm_email(request, email_address)
super(AutoadminAccountAdapter, self).confirm_email(
request=request, email_address=email_address
)
if email_address.email in ADMIN_EMAIL_ADDRESSES:
user = email_address.user
user.is_staff = user.is_superuser = True
user.save()
messages.add_message(
request, messages.INFO,
_('Welcome Admin! You have been given superuser privileges. '
'Use them with caution.')
messages.info(
request=request, message=_(
'Welcome Admin! You have been given superuser '
'privileges. Use them with caution.'
)
)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from django.conf import settings
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):

View File

@@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_EMAIL, DEFAULT_PASSWORD, DEFAULT_USERNAME
namespace = Namespace(name='autoadmin', label=_('Auto administrator'))
namespace = Namespace(label=_('Auto administrator'), name='autoadmin')
setting_email = namespace.add_setting(
global_name='AUTOADMIN_EMAIL', default=DEFAULT_EMAIL,

View File

@@ -1,9 +1,7 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.urls import reverse
from mayan.apps.common.settings import setting_home_view
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton
@@ -11,15 +9,18 @@ from ..models import AutoAdminSingleton
from .literals import TEST_FIRST_TIME_LOGIN_TEXT, TEST_MOCK_VIEW_TEXT
class AutoAdminViewCase(TestCase):
class AutoAdminViewCase(GenericViewTestCase):
auto_create_group = False
auto_create_users = False
auto_login_user = False
def setUp(self):
super(AutoAdminViewCase, self).setUp()
with mute_stdout():
AutoAdminSingleton.objects.create_autoadmin()
def _request_home_view(self):
return self.client.get(
reverse(setting_home_view.value), follow=True
)
return self.get(viewname=setting_home_view.value, follow=True)
def test_login_302_view(self):
response = self._request_home_view()
@@ -31,7 +32,7 @@ class AutoAdminViewCase(TestCase):
def test_login_ok_view(self):
autoadmin = AutoAdminSingleton.objects.get()
logged_in = self.client.login(
logged_in = self.login(
username=autoadmin.account,
password=autoadmin.password
)

View File

@@ -33,7 +33,7 @@ class APIDocumentCabinetListView(generics.ListAPIView):
mayan_object_permissions = {'GET': (permission_cabinet_view,)}
def get_queryset(self):
document = get_object_or_404(Document, pk=self.kwargs['pk'])
document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
AccessControlList.objects.check_access(
permissions=permission_document_view, user=self.request.user,
obj=document
@@ -135,12 +135,12 @@ class APICabinetDocumentListView(generics.ListCreateAPIView):
return context
def get_cabinet(self):
return get_object_or_404(klass=Cabinet, pk=self.kwargs['pk'])
return get_object_or_404(klass=Cabinet, pk=self.kwargs['cabinet_pk'])
def get_queryset(self):
cabinet = self.get_cabinet()
return AccessControlList.objects.filter_by_access(
return AccessControlList.objects.restrict_queryset(
permission_document_view, self.request.user,
queryset=cabinet.documents.all()
)
@@ -163,7 +163,7 @@ class APICabinetDocumentView(generics.RetrieveDestroyAPIView):
serializer_class = CabinetDocumentSerializer
def get_cabinet(self):
return get_object_or_404(klass=Cabinet, pk=self.kwargs['pk'])
return get_object_or_404(klass=Cabinet, pk=self.kwargs['cabinet_pk'])
def get_queryset(self):
return self.get_cabinet().documents.all()

View File

@@ -9,18 +9,18 @@ from mayan.apps.acls.permissions import (
)
from mayan.apps.common import (
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_object,
menu_sidebar
menu_secondary
)
from mayan.apps.common.classes import ModelAttribute
from mayan.apps.documents.search import document_page_search, document_search
from mayan.apps.navigation import SourceColumn
from .links import (
link_cabinet_add_document, link_cabinet_add_multiple_documents,
link_cabinet_child_add, link_cabinet_create, link_cabinet_delete,
link_cabinet_edit, link_cabinet_list, link_cabinet_view,
link_custom_acl_list, link_document_cabinet_list,
link_document_cabinet_remove, link_multiple_document_cabinet_remove
link_custom_acl_list, link_document_cabinet_add,
link_document_cabinet_list, link_document_cabinet_remove,
link_document_multiple_cabinet_add, link_document_multiple_cabinet_remove
)
from .menus import menu_cabinets
from .methods import method_get_document_cabinets
@@ -49,8 +49,8 @@ class CabinetsApp(MayanAppConfig):
app_label='documents', model_name='Document'
)
DocumentCabinet = self.get_model('DocumentCabinet')
Cabinet = self.get_model('Cabinet')
DocumentCabinet = self.get_model(model_name='DocumentCabinet')
Cabinet = self.get_model(model_name='Cabinet')
# Add explicit order_by as DocumentCabinet ordering Meta option has no
# effect.
@@ -75,23 +75,22 @@ class CabinetsApp(MayanAppConfig):
permission_cabinet_remove_document
)
)
ModelPermission.register_inheritance(
model=Cabinet, related='get_root',
)
#ModelPermission.register_inheritance(
# model=Cabinet, related='get_root',
#)
SourceColumn(
source=Document, label=_('Cabinets'),
func=lambda context: widget_document_cabinets(
document=context['object'], user=context['request'].user
), order=1
), order=1, label=_('Cabinets'), source=Document
)
document_page_search.add_model_field(
field='document_version__document__cabinets__label',
label=_('Cabinets')
label=_('Cabinets'),
field='document_version__document__cabinets__label'
)
document_search.add_model_field(
field='cabinets__label', label=_('Cabinets')
label=_('Cabinets'), field='cabinets__label'
)
menu_facet.bind_links(
@@ -108,8 +107,8 @@ class CabinetsApp(MayanAppConfig):
menu_multi_item.bind_links(
links=(
link_cabinet_add_multiple_documents,
link_multiple_document_cabinet_remove
link_document_multiple_cabinet_add,
link_document_multiple_cabinet_remove
), sources=(Document,)
)
menu_object.bind_links(
@@ -124,11 +123,11 @@ class CabinetsApp(MayanAppConfig):
link_cabinet_delete
), sources=(Cabinet,)
)
menu_sidebar.bind_links(
links=(link_cabinet_add_document, link_document_cabinet_remove),
menu_secondary.bind_links(
links=(link_document_cabinet_add, link_document_cabinet_remove),
sources=(
'cabinets:document_cabinet_list',
'cabinets:cabinet_add_document',
'cabinets:document_cabinet_add',
'cabinets:document_cabinet_remove'
)
)

View File

@@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.events import EventTypeNamespace
namespace = EventTypeNamespace(name='cabinets', label=_('Cabinets'))
namespace = EventTypeNamespace(label=_('Cabinets'), name='cabinets')
event_cabinets_add_document = namespace.add_event_type(
label=_('Document added to cabinet'), name='add_document'

View File

@@ -1,33 +1,14 @@
from __future__ import absolute_import, unicode_literals
import logging
from django import forms
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from .models import Cabinet
logger = logging.getLogger(__name__)
from mayan.apps.common.forms import FilteredSelectionForm
class CabinetListForm(forms.Form):
def __init__(self, *args, **kwargs):
help_text = kwargs.pop('help_text', None)
permission = kwargs.pop('permission', None)
queryset = kwargs.pop('queryset', Cabinet.objects.all())
user = kwargs.pop('user', None)
logger.debug('user: %s', user)
super(CabinetListForm, self).__init__(*args, **kwargs)
queryset = AccessControlList.objects.filter_by_access(
permission=permission, user=user, queryset=queryset
)
self.fields['cabinets'] = forms.ModelMultipleChoiceField(
label=_('Cabinets'), help_text=help_text,
queryset=queryset, required=False,
widget=forms.SelectMultiple(attrs={'class': 'select2'})
)
class CabinetListForm(FilteredSelectionForm):
class Meta:
allow_multiple = True
field_name = 'cabinets'
label = _('Cabinets')
required = False
widget_attributes = {'class': 'select2'}

View File

@@ -3,7 +3,25 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_cabinet = Icon(driver_name='fontawesome', symbol='columns')
icon_cabinet_add = Icon(driver_name='fontawesome', symbol='plus')
icon_cabinet_child_add = Icon(driver_name='fontawesome', symbol='plus')
icon_cabinet_create = Icon(driver_name='fontawesome', symbol='plus')
icon_cabinet_delete = Icon(driver_name='fontawesome', symbol='times')
icon_cabinet_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_cabinet_list = Icon(driver_name='fontawesome', symbol='columns')
icon_cabinet_view = Icon(driver_name='fontawesome', symbol='columns')
icon_document_cabinet_add = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='arrow-right'
)
icon_document_multiple_cabinet_add = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='arrow-right'
)
icon_document_cabinet_remove = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='minus'
)
icon_document_multiple_cabinet_remove = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='minus'
)

View File

@@ -9,8 +9,10 @@ from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.navigation import Link, get_cascade_condition
from .icons import (
icon_cabinet_add, icon_cabinet_child_add, icon_cabinet_create,
icon_cabinet_list
icon_cabinet_child_add, icon_cabinet_create, icon_cabinet_delete,
icon_cabinet_edit, icon_cabinet_list, icon_cabinet_view,
icon_document_cabinet_add, icon_document_cabinet_remove,
icon_document_multiple_cabinet_add, icon_document_multiple_cabinet_remove
)
from .permissions import (
permission_cabinet_add_document, permission_cabinet_create,
@@ -22,25 +24,27 @@ from .permissions import (
link_document_cabinet_list = Link(
args='resolved_object.pk', icon_class=icon_cabinet_list,
permissions=(permission_document_view,),
text=_('Cabinets'), view='cabinets:document_cabinet_list',
permission=permission_document_view, text=_('Cabinets'),
view='cabinets:document_cabinet_list',
)
link_document_cabinet_remove = Link(
args='resolved_object.pk',
permissions=(permission_cabinet_remove_document,),
args='resolved_object.pk', icon_class=icon_document_cabinet_remove,
permission=permission_cabinet_remove_document,
text=_('Remove from cabinets'), view='cabinets:document_cabinet_remove'
)
link_cabinet_add_document = Link(
args='object.pk', icon_class=icon_cabinet_add,
permissions=(permission_cabinet_add_document,), text=_('Add to cabinets'),
view='cabinets:cabinet_add_document',
link_document_cabinet_add = Link(
args='object.pk', icon_class=icon_document_cabinet_add,
permission=permission_cabinet_add_document, text=_('Add to cabinets'),
view='cabinets:document_cabinet_add',
)
link_cabinet_add_multiple_documents = Link(
text=_('Add to cabinets'), view='cabinets:cabinet_add_multiple_documents'
link_document_multiple_cabinet_add = Link(
icon_class=icon_document_multiple_cabinet_add, text=_('Add to cabinets'),
view='cabinets:document_multiple_cabinet_add'
)
link_multiple_document_cabinet_remove = Link(
link_document_multiple_cabinet_remove = Link(
icon_class=icon_document_multiple_cabinet_remove,
text=_('Remove from cabinets'),
view='cabinets:multiple_document_cabinet_remove'
view='cabinets:document_multiple_cabinet_remove'
)
# Cabinet links
@@ -57,19 +61,21 @@ link_custom_acl_list.condition = cabinet_is_root
link_cabinet_child_add = Link(
args='object.pk', icon_class=icon_cabinet_child_add,
permissions=(permission_cabinet_create,), text=_('Add new level'),
permission=permission_cabinet_create, text=_('Add new level'),
view='cabinets:cabinet_child_add'
)
link_cabinet_create = Link(
icon_class=icon_cabinet_create, permissions=(permission_cabinet_create,),
icon_class=icon_cabinet_create, permission=permission_cabinet_create,
text=_('Create cabinet'), view='cabinets:cabinet_create'
)
link_cabinet_delete = Link(
args='object.pk', permissions=(permission_cabinet_delete,),
tags='dangerous', text=_('Delete'), view='cabinets:cabinet_delete'
args='object.pk', icon_class=icon_cabinet_delete,
permission=permission_cabinet_delete, tags='dangerous',
text=_('Delete'), view='cabinets:cabinet_delete'
)
link_cabinet_edit = Link(
args='object.pk', permissions=(permission_cabinet_edit,), text=_('Edit'),
args='object.pk', icon_class=icon_cabinet_edit,
permission=permission_cabinet_edit, text=_('Edit'),
view='cabinets:cabinet_edit'
)
link_cabinet_list = Link(
@@ -80,6 +86,7 @@ link_cabinet_list = Link(
view='cabinets:cabinet_list'
)
link_cabinet_view = Link(
args='object.pk', permissions=(permission_cabinet_view,), text=_('Details'),
args='object.pk', icon_class=icon_cabinet_view,
permission=permission_cabinet_view, text=_('Details'),
view='cabinets:cabinet_view'
)

View File

@@ -53,11 +53,13 @@ class Cabinet(MPTTModel):
"""
self.documents.add(document)
event_cabinets_add_document.commit(
action_object=self, actor=user, target=document
actor=user, action_object=self, target=document
)
def get_absolute_url(self):
return reverse('cabinets:cabinet_view', args=(self.pk,))
return reverse(
viewname='cabinets:cabinet_view', kwargs={'cabinet_pk': self.pk}
)
def get_document_count(self, user):
"""
@@ -71,8 +73,9 @@ class Cabinet(MPTTModel):
Provide a queryset of the documents in a cabinet. The queryset is
filtered by access.
"""
return AccessControlList.objects.filter_by_access(
permission_document_view, user, queryset=self.documents
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.documents,
user=user
)
def get_full_path(self):
@@ -93,7 +96,7 @@ class Cabinet(MPTTModel):
"""
self.documents.remove(document)
event_cabinets_remove_document.commit(
action_object=self, actor=user, target=document
actor=user, action_object=self, target=document
)
def validate_unique(self, exclude=None):
@@ -105,9 +108,13 @@ class Cabinet(MPTTModel):
"""
with transaction.atomic():
if connection.vendor == 'oracle':
queryset = Cabinet.objects.filter(parent=self.parent, label=self.label)
queryset = Cabinet.objects.filter(
parent=self.parent, label=self.label
)
else:
queryset = Cabinet.objects.select_for_update().filter(parent=self.parent, label=self.label)
queryset = Cabinet.objects.select_for_update().filter(
parent=self.parent, label=self.label
)
if queryset.exists():
params = {

View File

@@ -9,20 +9,20 @@ namespace = PermissionNamespace(label=_('Cabinets'), name='cabinets')
# Translators: this refers to the permission that will allow users to add
# documents to cabinets.
permission_cabinet_add_document = namespace.add_permission(
name='cabinet_add_document', label=_('Add documents to cabinets')
label=_('Add documents to cabinets'), name='cabinet_add_document'
)
permission_cabinet_create = namespace.add_permission(
name='cabinet_create', label=_('Create cabinets')
label=_('Create cabinets'), name='cabinet_create'
)
permission_cabinet_delete = namespace.add_permission(
name='cabinet_delete', label=_('Delete cabinets')
label=_('Delete cabinets'), name='cabinet_delete'
)
permission_cabinet_edit = namespace.add_permission(
name='cabinet_edit', label=_('Edit cabinets')
label=_('Edit cabinets'), name='cabinet_edit'
)
permission_cabinet_remove_document = namespace.add_permission(
name='cabinet_remove_document', label=_('Remove documents from cabinets')
label=_('Remove documents from cabinets'), name='cabinet_remove_document'
)
permission_cabinet_view = namespace.add_permission(
name='cabinet_view', label=_('View cabinets')
label=_('View cabinets'), name='cabinet_view'
)

View File

@@ -9,7 +9,7 @@ from .permissions import permission_cabinet_view
cabinet_search = SearchModel(
app_label='cabinets', model_name='Cabinet',
permission=permission_cabinet_view,
serializer_string='mayan.apps.cabinets.serializers.CabinetSerializer'
serializer_path='mayan.apps.cabinets.serializers.CabinetSerializer'
)
cabinet_search.add_model_field(

View File

@@ -54,7 +54,7 @@ class CabinetSerializer(serializers.ModelSerializer):
def get_parent_url(self, obj):
if obj.parent:
return reverse(
'rest_api:cabinet-detail', args=(obj.parent.pk,),
viewname='rest_api:cabinet-detail', kwargs={'cabinet_pk': obj.parent.pk},
format=self.context['format'],
request=self.context.get('request')
)
@@ -167,9 +167,10 @@ class CabinetDocumentSerializer(DocumentSerializer):
def get_cabinet_document_url(self, instance):
return reverse(
'rest_api:cabinet-document', args=(
self.context['cabinet'].pk, instance.pk
), request=self.context['request'], format=self.context['format']
viewname='rest_api:cabinet-document', kwargs={
'cabinet_pk': self.context['cabinet'].pk,
'document_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.encoding import force_text
from rest_framework import status
@@ -39,8 +38,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
self.document_2 = self.upload_document()
def test_cabinet_create(self):
response = self.client.post(
reverse('rest_api:cabinet-list'), {'label': TEST_CABINET_LABEL}
response = self.post(
viewname='rest_api:cabinet-list',
data={'label': TEST_CABINET_LABEL}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -53,8 +53,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_create_with_single_document(self):
response = self.client.post(
reverse('rest_api:cabinet-list'), {
response = self.post(
viewname='rest_api:cabinet-list', data={
'label': TEST_CABINET_LABEL, 'documents_pk_list': '{}'.format(
self.document.pk
)
@@ -73,8 +73,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_create_with_multiple_documents(self):
response = self.client.post(
reverse('rest_api:cabinet-list'), {
response = self.post(
viewname='rest_api:cabinet-list', data={
'label': TEST_CABINET_LABEL,
'documents_pk_list': '{},{}'.format(
self.document.pk, self.document_2.pk
@@ -102,11 +102,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.client.delete(
reverse(
'rest_api:cabinet-document',
args=(cabinet.pk, self.document.pk)
)
response = self.delete(
viewname='rest_api:cabinet-document',
kwargs={'cabinet_pk': cabinet.pk, 'document_pk': self.document.pk}
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -116,11 +114,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.client.get(
reverse(
'rest_api:cabinet-document',
args=(cabinet.pk, self.document.pk)
)
response = self.get(
viewname='rest_api:cabinet-document',
kwargs={'cabinet_pk': cabinet.pk, 'document_pk': self.document.pk}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -132,8 +128,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.client.get(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,))
response = self.get(
viewname='rest_api:cabinet-document-list',
kwargs={'cabinet_pk': cabinet.pk}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -144,8 +141,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_delete(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.client.delete(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,))
response = self.delete(
viewname='rest_api:cabinet-detail',
kwargs={'cabinet_pk': cabinet.pk}
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -154,9 +152,10 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_edit_via_patch(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.client.patch(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,)),
{'label': TEST_CABINET_EDITED_LABEL}
response = self.patch(
viewname='rest_api:cabinet-detail',
kwargs={'cabinet_pk': cabinet.pk},
data={'label': TEST_CABINET_EDITED_LABEL}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -167,9 +166,10 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_edit_via_put(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.client.put(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,)),
{'label': TEST_CABINET_EDITED_LABEL}
response = self.put(
viewname='rest_api:cabinet-detail',
kwargs={'cabinet_pk': cabinet.pk},
data={'label': TEST_CABINET_EDITED_LABEL}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -180,8 +180,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_add_document(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.client.post(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,)), {
response = self.post(
viewname='rest_api:cabinet-document-list',
kwargs={'cabinet_pk': cabinet.pk}, data={
'documents_pk_list': '{}'.format(self.document.pk)
}
)
@@ -194,8 +195,10 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_add_multiple_documents(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.client.post(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,)), {
response = self.post(
viewname='rest_api:cabinet-document-list',
kwargs={'cabinet_pk': cabinet.pk},
data={
'documents_pk_list': '{},{}'.format(
self.document.pk, self.document_2.pk
)
@@ -215,8 +218,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
label=TEST_CABINET_LABEL, parent=cabinet
)
response = self.client.get(
reverse('rest_api:cabinet-list')
response = self.get(
viewname='rest_api:cabinet-list'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['results'][0]['label'], cabinet.label)
@@ -226,12 +229,10 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet.documents.add(self.document)
response = self.client.delete(
reverse(
'rest_api:cabinet-document', args=(
cabinet.pk, self.document.pk
)
),
response = self.delete(
viewname='rest_api:cabinet-document', kwargs={
'cabinet_pk': cabinet.pk, 'document_pk': self.document.pk
}
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(cabinet.documents.count(), 0)

View File

@@ -53,14 +53,15 @@ class CabinetViewTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_delete_cabinet(self):
return self.post(
viewname='cabinets:cabinet_delete', args=(self.cabinet.pk,)
viewname='cabinets:cabinet_delete',
kwargs={'cabinet_pk': self.cabinet.pk}
)
def test_cabinet_delete_view_no_permission(self):
self._create_cabinet()
response = self._request_delete_cabinet()
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 404)
self.assertEqual(Cabinet.objects.count(), 1)
def test_cabinet_delete_view_with_access(self):
@@ -74,7 +75,7 @@ class CabinetViewTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_edit_cabinet(self):
return self.post(
viewname='cabinets:cabinet_edit', args=(self.cabinet.pk,), data={
viewname='cabinets:cabinet_edit', kwargs={'cabinet_pk': self.cabinet.pk}, data={
'label': TEST_CABINET_EDITED_LABEL
}
)
@@ -83,7 +84,7 @@ class CabinetViewTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self._create_cabinet()
response = self._request_edit_cabinet()
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 404)
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.label, TEST_CABINET_LABEL)
@@ -125,16 +126,14 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _add_document_to_cabinet(self):
return self.post(
viewname='cabinets:cabinet_add_document', args=(
self.document.pk,
), data={'cabinets': self.cabinet.pk}
viewname='cabinets:document_cabinet_add', kwargs={
'document_pk': self.document.pk
}, data={'cabinets': self.cabinet.pk}
)
def test_cabinet_add_document_view_no_permission(self):
self._create_cabinet()
self.grant_permission(permission=permission_cabinet_view)
response = self._add_document_to_cabinet()
self.assertContains(
@@ -143,10 +142,37 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_access(self):
def test_cabinet_add_document_view_with_cabinet_access(self):
self._create_cabinet()
self.grant_access(
obj=self.cabinet, permission=permission_cabinet_add_document
)
response = self._add_document_to_cabinet()
self.assertContains(
response, text='Select a valid choice.', status_code=404
)
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_document_access(self):
self._create_cabinet()
self.grant_access(
obj=self.cabinet, permission=permission_cabinet_add_document
)
response = self._add_document_to_cabinet()
self.assertContains(
response, text='Select a valid choice.', status_code=404
)
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_full_access(self):
self._create_cabinet()
self.grant_access(obj=self.cabinet, permission=permission_cabinet_view)
self.grant_access(
obj=self.cabinet, permission=permission_cabinet_add_document
)
@@ -166,7 +192,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_add_multiple_documents_to_cabinet(self):
return self.post(
viewname='cabinets:cabinet_add_multiple_documents', data={
viewname='cabinets:document_multiple_cabinet_add', data={
'id_list': (self.document.pk,), 'cabinets': self.cabinet.pk
}
)
@@ -174,8 +200,6 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def test_cabinet_add_multiple_documents_view_no_permission(self):
self._create_cabinet()
self.grant_permission(permission=permission_cabinet_view)
response = self._request_add_multiple_documents_to_cabinet()
self.assertContains(
@@ -184,7 +208,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_multiple_documents_view_with_access(self):
def test_cabinet_add_multiple_documents_view_with_full_access(self):
self._create_cabinet()
self.grant_access(
@@ -206,7 +230,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_remove_document_from_cabinet(self):
return self.post(
viewname='cabinets:document_cabinet_remove',
args=(self.document.pk,), data={
kwargs={'document_pk': self.document.pk}, data={
'cabinets': (self.cabinet.pk,),
}
)
@@ -225,7 +249,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 1)
def test_cabinet_remove_document_view_with_access(self):
def test_cabinet_remove_document_view_with_full_access(self):
self._create_cabinet()
self.cabinet.documents.add(self.document)
@@ -245,7 +269,8 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_document_cabinet_list(self):
return self.get(
viewname='cabinets:document_cabinet_list', args=(self.document.pk,)
viewname='cabinets:document_cabinet_list',
kwargs={'document_pk': self.document.pk}
)
def test_document_cabinet_list_view_no_permission(self):
@@ -253,10 +278,10 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.documents.add(self.document)
response = self._request_document_cabinet_list()
self.assertNotContains(
response=response, text=self.document.label, status_code=403
response=response, text=self.document.label, status_code=404
)
self.assertNotContains(
response=response, text=self.cabinet.label, status_code=403
response=response, text=self.cabinet.label, status_code=404
)
def test_document_cabinet_list_view_with_cabinet_access(self):
@@ -265,10 +290,10 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.grant_access(obj=self.cabinet, permission=permission_cabinet_view)
response = self._request_document_cabinet_list()
self.assertNotContains(
response=response, text=self.document.label, status_code=403
response=response, text=self.document.label, status_code=404
)
self.assertNotContains(
response=response, text=self.cabinet.label, status_code=403
response=response, text=self.cabinet.label, status_code=404
)
def test_document_cabinet_list_view_with_document_access(self):

View File

@@ -35,7 +35,7 @@ class CabinetDocumentUploadTestCase(GenericDocumentViewTestCase):
def _request_upload_interactive_document_create_view(self):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
return self.post(
viewname='sources:upload_interactive', args=(self.source.pk,),
viewname='sources:upload_interactive', kwargs={'source_pk': self.source.pk},
data={
'document_type_id': self.document_type.pk,
'source-file': file_object,

View File

@@ -14,61 +14,74 @@ from .views import (
)
urlpatterns = [
url(r'^list/$', CabinetListView.as_view(), name='cabinet_list'),
url(
r'^(?P<pk>\d+)/child/add/$', CabinetChildAddView.as_view(),
name='cabinet_child_add'
),
url(r'^create/$', CabinetCreateView.as_view(), name='cabinet_create'),
url(
r'^(?P<pk>\d+)/edit/$', CabinetEditView.as_view(), name='cabinet_edit'
regex=r'^cabinets/$', name='cabinet_list',
view=CabinetListView.as_view()
),
url(
r'^(?P<pk>\d+)/delete/$', CabinetDeleteView.as_view(),
name='cabinet_delete'
),
url(r'^(?P<pk>\d+)/$', CabinetDetailView.as_view(), name='cabinet_view'),
url(
r'^document/(?P<pk>\d+)/cabinet/add/$',
DocumentAddToCabinetView.as_view(), name='cabinet_add_document'
regex=r'^cabinets/create/$', name='cabinet_create',
view=CabinetCreateView.as_view()
),
url(
r'^document/multiple/cabinet/add/$',
DocumentAddToCabinetView.as_view(),
name='cabinet_add_multiple_documents'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/$', name='cabinet_view',
view=CabinetDetailView.as_view()
),
url(
r'^document/(?P<pk>\d+)/cabinet/remove/$',
DocumentRemoveFromCabinetView.as_view(), name='document_cabinet_remove'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/delete/$', name='cabinet_delete',
view=CabinetDeleteView.as_view()
),
url(
r'^document/multiple/cabinet/remove/$',
DocumentRemoveFromCabinetView.as_view(),
name='multiple_document_cabinet_remove'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/edit/$', name='cabinet_edit',
view=CabinetEditView.as_view()
),
url(
r'^document/(?P<pk>\d+)/cabinet/list/$',
DocumentCabinetListView.as_view(), name='document_cabinet_list'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/child/add/$',
name='cabinet_child_add', view=CabinetChildAddView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/add/$',
name='document_cabinet_add', view=DocumentAddToCabinetView.as_view()
),
url(
regex=r'^documents/multiple/cabinets/add/$',
name='document_multiple_cabinet_add',
view=DocumentAddToCabinetView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/remove/$',
name='document_cabinet_remove',
view=DocumentRemoveFromCabinetView.as_view()
),
url(
regex=r'^documents/multiple/cabinets/remove/$',
name='document_multiple_cabinet_remove',
view=DocumentRemoveFromCabinetView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/list/$',
name='document_cabinet_list', view=DocumentCabinetListView.as_view()
),
]
api_urls = [
url(
r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
APICabinetDocumentView.as_view(), name='cabinet-document'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/documents/(?P<document_pk>\d+)/$',
name='cabinet-document', view=APICabinetDocumentView.as_view()
),
url(
r'^cabinets/(?P<pk>[0-9]+)/documents/$',
APICabinetDocumentListView.as_view(), name='cabinet-document-list'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/documents/$',
name='cabinet-document-list', view=APICabinetDocumentListView.as_view()
),
url(
r'^cabinets/(?P<pk>[0-9]+)/$', APICabinetView.as_view(),
name='cabinet-detail'
regex=r'^cabinets/(?P<cabinet_pk>\d+)/$', name='cabinet-detail',
view=APICabinetView.as_view()
),
url(r'^cabinets/$', APICabinetListView.as_view(), name='cabinet-list'),
url(
r'^documents/(?P<pk>[0-9]+)/cabinets/$',
APIDocumentCabinetListView.as_view(), name='document-cabinet-list'
regex=r'^cabinets/$', name='cabinet-list',
view=APICabinetListView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/$',
name='document-cabinet-list', view=APIDocumentCabinetListView.as_view()
),
]

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.views import (
from mayan.apps.common.generics import (
MultipleObjectFormActionView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
)
@@ -21,7 +21,7 @@ from mayan.apps.documents.views import DocumentListView
from .forms import CabinetListForm
from .icons import icon_cabinet
from .links import (
link_cabinet_add_document, link_cabinet_child_add, link_cabinet_create
link_cabinet_child_add, link_cabinet_create, link_document_cabinet_add
)
from .models import Cabinet
from .permissions import (
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
class CabinetCreateView(SingleObjectCreateView):
fields = ('label',)
model = Cabinet
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
post_action_redirect = reverse_lazy(viewname='cabinets:cabinet_list')
view_permission = permission_cabinet_create
def get_extra_context(self):
@@ -64,8 +64,8 @@ class CabinetChildAddView(SingleObjectCreateView):
cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs)
AccessControlList.objects.check_access(
permissions=permission_cabinet_edit, user=self.request.user,
obj=cabinet.get_root()
obj=cabinet.get_root(), permission=permission_cabinet_edit,
user=self.request.user, raise_404=True
)
return cabinet
@@ -81,7 +81,9 @@ class CabinetChildAddView(SingleObjectCreateView):
class CabinetDeleteView(SingleObjectDeleteView):
model = Cabinet
object_permission = permission_cabinet_delete
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
object_permission_raise_404 = True
pk_url_kwarg = 'cabinet_pk'
post_action_redirect = reverse_lazy(viewname='cabinets:cabinet_list')
def get_extra_context(self):
return {
@@ -94,9 +96,10 @@ class CabinetDetailView(DocumentListView):
template_name = 'cabinets/cabinet_details.html'
def get_document_queryset(self):
queryset = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.request.user,
queryset=self.get_object().documents.all()
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=self.get_object().documents.all(),
user=self.request.user
)
return queryset
@@ -133,7 +136,9 @@ class CabinetDetailView(DocumentListView):
return context
def get_object(self):
cabinet = get_object_or_404(klass=Cabinet, pk=self.kwargs['pk'])
cabinet = get_object_or_404(
klass=Cabinet, pk=self.kwargs['cabinet_pk']
)
if cabinet.is_root_node():
permission_object = cabinet
@@ -141,8 +146,8 @@ class CabinetDetailView(DocumentListView):
permission_object = cabinet.get_root()
AccessControlList.objects.check_access(
permissions=permission_cabinet_view, user=self.request.user,
obj=permission_object
obj=permission_object, permission=permission_cabinet_view,
user=self.request.user, raise_404=True
)
return cabinet
@@ -152,7 +157,9 @@ class CabinetEditView(SingleObjectEditView):
fields = ('label',)
model = Cabinet
object_permission = permission_cabinet_edit
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
object_permission_raise_404 = True
pk_url_kwarg = 'cabinet_pk'
post_action_redirect = reverse_lazy(viewname='cabinets:cabinet_list')
def get_extra_context(self):
return {
@@ -180,7 +187,7 @@ class CabinetListView(SingleObjectListView):
'no_results_title': _('No cabinets available'),
}
def get_object_list(self):
def get_source_queryset(self):
# Add explicit ordering of root nodes since the queryset returned
# is not affected by the model's order Meta option.
return Cabinet.objects.root_nodes().order_by('label')
@@ -188,11 +195,13 @@ class CabinetListView(SingleObjectListView):
class DocumentCabinetListView(CabinetListView):
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
self.document = get_object_or_404(
klass=Document, pk=self.kwargs['document_pk']
)
AccessControlList.objects.check_access(
permissions=permission_document_view, user=request.user,
obj=self.document
obj=self.document, permission=permission_document_view,
user=request.user, raise_404=True
)
return super(DocumentCabinetListView, self).dispatch(
@@ -203,7 +212,7 @@ class DocumentCabinetListView(CabinetListView):
return {
'hide_link': True,
'no_results_icon': icon_cabinet,
'no_results_main_link': link_cabinet_add_document.resolve(
'no_results_main_link': link_document_cabinet_add.resolve(
context=RequestContext(
request=self.request, dict_={'object': self.document}
)
@@ -218,7 +227,7 @@ class DocumentCabinetListView(CabinetListView):
'title': _('Cabinets containing document: %s') % self.document,
}
def get_object_list(self):
def get_source_queryset(self):
return self.document.get_cabinets().all()
@@ -226,7 +235,8 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
form_class = CabinetListForm
model = Document
object_permission = permission_cabinet_add_document
success_message = _(
pk_url_kwarg = 'document_pk'
success_message_singular = _(
'Add to cabinet request performed on %(count)d document'
)
success_message_plural = _(
@@ -285,12 +295,12 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access(
obj=cabinet, permissions=permission_cabinet_add_document,
user=self.request.user
obj=cabinet, permission=permission_cabinet_add_document,
user=self.request.user, raise_404=True
)
if cabinet in cabinet_membership:
messages.warning(
self.request, _(
request=self.request, message=_(
'Document: %(document)s is already in '
'cabinet: %(cabinet)s.'
) % {
@@ -302,7 +312,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
document=instance, user=self.request.user
)
messages.success(
self.request, _(
request=self.request, message=_(
'Document: %(document)s added to cabinet: '
'%(cabinet)s successfully.'
) % {
@@ -315,7 +325,8 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
form_class = CabinetListForm
model = Document
object_permission = permission_cabinet_remove_document
success_message = _(
pk_url_kwarg = 'document_pk'
success_message_singular = _(
'Remove from cabinet request performed on %(count)d document'
)
success_message_plural = _(
@@ -372,13 +383,13 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access(
obj=cabinet, permissions=permission_cabinet_remove_document,
user=self.request.user
obj=cabinet, permission=permission_cabinet_remove_document,
user=self.request.user, raise_404=True
)
if cabinet not in cabinet_membership:
messages.warning(
self.request, _(
request=self.request, message=_(
'Document: %(document)s is not in cabinet: '
'%(cabinet)s.'
) % {
@@ -390,7 +401,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
document=instance, user=self.request.user
)
messages.success(
self.request, _(
request=self.request, message=_(
'Document: %(document)s removed from cabinet: '
'%(cabinet)s.'
) % {

View File

@@ -42,8 +42,9 @@ def widget_document_cabinets(document, user):
app_label='acls', model_name='AccessControlList'
)
cabinets = AccessControlList.objects.filter_by_access(
permission_cabinet_view, user, queryset=document.get_cabinets().all()
cabinets = AccessControlList.objects.restrict_queryset(
queryset=document.get_cabinets().all(),
permission=permission_cabinet_view, user=user
)
return format_html_join(

View File

@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.sources.wizards import WizardStep
from .forms import CabinetListForm
from .models import Cabinet
from .permissions import permission_cabinet_add_document
@@ -28,7 +29,7 @@ class WizardStepCabinets(WizardStep):
@classmethod
def done(cls, wizard):
result = {}
cleaned_data = wizard.get_cleaned_data_for_step(cls.name)
cleaned_data = wizard.get_cleaned_data_for_step(step=cls.name)
if cleaned_data:
result['cabinets'] = [
force_text(cabinet.pk) for cabinet in cleaned_data['cabinets']
@@ -41,6 +42,7 @@ class WizardStepCabinets(WizardStep):
return {
'help_text': _('Cabinets to which the document will be added.'),
'permission': permission_cabinet_add_document,
'queryset': Cabinet.objects.all(),
'user': wizard.request.user
}

View File

@@ -7,7 +7,7 @@ from mayan.apps.documents.permissions import permission_document_view
from .models import DocumentCheckout
from .permissions import (
permission_document_checkin, permission_document_checkin_override,
permission_document_check_in, permission_document_check_in_override,
permission_document_checkout_detail_view
)
from .serializers import (
@@ -33,11 +33,11 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
return DocumentCheckoutSerializer
def get_queryset(self):
filtered_documents = AccessControlList.objects.filter_by_access(
filtered_documents = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
filtered_documents = AccessControlList.objects.filter_by_access(
filtered_documents = AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view, user=self.request.user,
queryset=filtered_documents
)
@@ -56,11 +56,11 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
def get_queryset(self):
if self.request.method == 'GET':
filtered_documents = AccessControlList.objects.filter_by_access(
filtered_documents = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
filtered_documents = AccessControlList.objects.filter_by_access(
filtered_documents = AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view, user=self.request.user,
queryset=filtered_documents
)
@@ -78,12 +78,12 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
if document.checkout_info().user == request.user:
AccessControlList.objects.check_access(
permissions=permission_document_checkin, user=request.user,
permissions=permission_document_check_in, user=request.user,
obj=document
)
else:
AccessControlList.objects.check_access(
permissions=permission_document_checkin_override,
permissions=permission_document_check_in_override,
user=request.user, obj=document
)

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls import ModelPermission
from mayan.apps.common import (
MayanAppConfig, menu_facet, menu_main, menu_sidebar
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_secondary
)
from mayan.apps.dashboards.dashboards import dashboard_main
from mayan.apps.events import ModelEventType
@@ -22,9 +22,11 @@ from .events import (
event_document_check_out, event_document_forceful_check_in
)
from .handlers import handler_check_new_version_creation
from .hooks import hook_is_new_version_allowed
from .links import (
link_checkin_document, link_checkout_document, link_checkout_info,
link_checkout_list
link_document_check_in, link_document_checkout, link_document_checkout_info,
link_document_checkout_list, link_document_multiple_check_in,
link_document_multiple_checkout
)
from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL
from .methods import (
@@ -32,7 +34,7 @@ from .methods import (
method_is_checked_out
)
from .permissions import (
permission_document_checkin, permission_document_checkin_override,
permission_document_check_in, permission_document_check_in_override,
permission_document_checkout, permission_document_checkout_detail_view
)
from .queues import * # NOQA
@@ -69,6 +71,10 @@ class CheckoutsApp(MayanAppConfig):
name='is_checked_out', value=method_is_checked_out
)
DocumentVersion.register_pre_save_hook(
func=hook_is_new_version_allowed
)
ModelEventType.register(
model=Document, event_types=(
event_document_auto_check_in, event_document_check_in,
@@ -79,8 +85,8 @@ class CheckoutsApp(MayanAppConfig):
ModelPermission.register(
model=Document, permissions=(
permission_document_checkout,
permission_document_checkin,
permission_document_checkin_override,
permission_document_check_in,
permission_document_check_in_override,
permission_document_checkout_detail_view
)
)
@@ -115,13 +121,18 @@ class CheckoutsApp(MayanAppConfig):
widget=DashboardWidgetTotalCheckouts, order=-1
)
menu_facet.bind_links(links=(link_checkout_info,), sources=(Document,))
menu_main.bind_links(links=(link_checkout_list,), position=98)
menu_sidebar.bind_links(
links=(link_checkout_document, link_checkin_document),
menu_facet.bind_links(links=(link_document_checkout_info,), sources=(Document,))
menu_main.bind_links(links=(link_document_checkout_list,), position=98)
menu_multi_item.bind_links(
links=(
link_document_multiple_check_in, link_document_multiple_checkout
), sources=(Document,)
)
menu_secondary.bind_links(
links=(link_document_checkout, link_document_check_in),
sources=(
'checkouts:checkout_info', 'checkouts:checkout_document',
'checkouts:checkin_document'
'checkouts:document_checkout_info', 'checkouts:document_checkout',
'checkouts:document_check_in'
)
)

View File

@@ -14,7 +14,7 @@ from .permissions import permission_document_checkout_detail_view
class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
icon_class = icon_dashboard_checkouts
label = _('Checkedout documents')
link = reverse_lazy('checkouts:checkout_list')
link = reverse_lazy(viewname='checkouts:document_checkout_list')
def render(self, request):
AccessControlList = apps.get_model(
@@ -23,14 +23,16 @@ class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
DocumentCheckout = apps.get_model(
app_label='checkouts', model_name='DocumentCheckout'
)
queryset = AccessControlList.objects.filter_by_access(
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
queryset = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=request.user,
queryset=queryset
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=queryset,
user=request.user
)
self.count = queryset.count()
return super(DashboardWidgetTotalCheckouts, self).render(request)
return super(DashboardWidgetTotalCheckouts, self).render(
request=request
)

View File

@@ -4,19 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.events import EventTypeNamespace
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
namespace = EventTypeNamespace(label=_('Checkouts'), name='checkouts')
event_document_auto_check_in = namespace.add_event_type(
name='document_auto_check_in',
label=_('Document automatically checked in')
label=_('Document automatically checked in'),
name='document_auto_check_in'
)
event_document_check_in = namespace.add_event_type(
name='document_check_in', label=_('Document checked in')
label=_('Document checked in'), name='document_check_in'
)
event_document_check_out = namespace.add_event_type(
name='document_check_out', label=_('Document checked out')
label=_('Document checked out'), name='document_check_out'
)
event_document_forceful_check_in = namespace.add_event_type(
name='document_forceful_check_in',
label=_('Document forcefully checked in')
label=_('Document forcefully checked in'),
name='document_forceful_check_in'
)

View File

@@ -13,6 +13,6 @@ def handler_check_new_version_creation(sender, instance, **kwargs):
app_label='checkouts', model_name='NewVersionBlock'
)
if NewVersionBlock.objects.is_blocked(instance.document) and not instance.pk:
if NewVersionBlock.objects.is_blocked(document=instance.document) and not instance.pk:
# Block only new versions (no pk), not existing version being updated.
raise NewDocumentVersionNotAllowed

View File

@@ -0,0 +1,13 @@
from __future__ import unicode_literals
from django.apps import apps
def hook_is_new_version_allowed(document_version):
NewVersionBlock = apps.get_model(
app_label='checkouts', model_name='NewVersionBlock'
)
NewVersionBlock.objects.new_versions_allowed(
document=document_version.document
)

View File

@@ -2,6 +2,14 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_checkin_document = Icon(
driver_name='fontawesome-dual', primary_symbol='shopping-cart',
secondary_symbol='minus'
)
icon_checkout_document = Icon(
driver_name='fontawesome-dual', primary_symbol='shopping-cart',
secondary_symbol='plus'
)
icon_checkout_info = Icon(driver_name='fontawesome', symbol='shopping-cart')
icon_dashboard_checkouts = Icon(
driver_name='fontawesome', symbol='shopping-cart'

View File

@@ -4,10 +4,12 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation import Link
from .icons import icon_checkout_info
from .icons import (
icon_checkin_document, icon_checkout_document, icon_checkout_info
)
from .permissions import (
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout
permission_document_check_in, permission_document_checkout,
permission_document_checkout_detail_view
)
@@ -27,24 +29,32 @@ def is_not_checked_out(context):
return True
link_checkout_list = Link(
link_document_checkout_list = Link(
icon_class=icon_checkout_info, text=_('Checkouts'),
view='checkouts:checkout_list'
view='checkouts:document_checkout_list'
)
link_checkout_document = Link(
args='object.pk', condition=is_not_checked_out,
permissions=(permission_document_checkout,),
text=_('Check out document'), view='checkouts:checkout_document',
link_document_checkout = Link(
condition=is_not_checked_out, icon_class=icon_checkout_document,
kwargs={'document_id': 'object.pk'},
permission=permission_document_checkout, text=_('Check out document'),
view='checkouts:document_checkout',
)
link_checkin_document = Link(
args='object.pk', condition=is_checked_out, permissions=(
permission_document_checkin, permission_document_checkin_override
), text=_('Check in document'), view='checkouts:checkin_document',
link_document_multiple_checkout = Link(
icon_class=icon_checkout_document,
permission=permission_document_checkout, text=_('Check out'),
view='checkouts:document_multiple_checkout',
)
link_checkout_info = Link(
args='resolved_object.pk', icon_class=icon_checkout_info, permissions=(
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout
), text=_('Check in/out'), view='checkouts:checkout_info',
link_document_check_in = Link(
condition=is_checked_out, icon_class=icon_checkin_document,
kwargs={'document_id': 'object.pk'}, permission=permission_document_check_in,
text=_('Check in document'), view='checkouts:document_check_in',
)
link_document_multiple_check_in = Link(
icon_class=icon_checkin_document, permission=permission_document_check_in,
text=_('Check in'), view='checkouts:document_multiple_check_in',
)
link_document_checkout_info = Link(
icon_class=icon_checkout_info, kwargs={'document_id': 'resolved_object.pk'},
permission=permission_document_checkout_detail_view,
text=_('Check in/out'), view='checkouts:document_checkout_info',
)

View File

@@ -2,48 +2,54 @@ from __future__ import absolute_import, unicode_literals
import logging
from django.apps import apps
from django.db import models
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document
from .events import (
event_document_auto_check_in, event_document_check_in,
event_document_forceful_check_in
)
from .exceptions import DocumentNotCheckedOut
from .exceptions import DocumentNotCheckedOut, NewDocumentVersionNotAllowed
from .literals import STATE_CHECKED_IN, STATE_CHECKED_OUT
from .permissions import permission_document_check_in_override
logger = logging.getLogger(__name__)
class DocumentCheckoutManager(models.Manager):
def are_document_new_versions_allowed(self, document, user=None):
try:
checkout_info = self.document_checkout_info(document)
except DocumentNotCheckedOut:
return True
else:
return not checkout_info.block_new_version
def check_in_document(self, document, user=None):
try:
document_checkout = self.model.objects.get(document=document)
except self.model.DoesNotExist:
raise DocumentNotCheckedOut
raise DocumentNotCheckedOut(
_('Document not checked out.')
)
else:
if user:
if self.get_document_checkout_info(document).user != user:
event_document_forceful_check_in.commit(
actor=user, target=document
)
with transaction.atomic():
if user:
if self.get_document_checkout_info(document=document).user != user:
try:
AccessControlList.objects.check_access(
obj=document, permission=permission_document_check_in_override,
user=user
)
except PermissionDenied:
return
else:
event_document_forceful_check_in.commit(
actor=user, target=document
)
else:
event_document_check_in.commit(actor=user, target=document)
else:
event_document_check_in.commit(actor=user, target=document)
else:
event_document_auto_check_in.commit(target=document)
event_document_auto_check_in.commit(target=document)
document_checkout.delete()
document_checkout.delete()
def check_in_expired_check_outs(self):
for document in self.get_expired_check_outs():
@@ -51,21 +57,16 @@ class DocumentCheckoutManager(models.Manager):
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
return self.create(
document=document, expiration_datetime=expiration_datetime,
user=user, block_new_version=block_new_version
block_new_version=block_new_version, document=document,
expiration_datetime=expiration_datetime, user=user
)
def checked_out_documents(self):
return Document.objects.filter(
pk__in=self.model.objects.all().values_list(
'document__pk', flat=True
)
pk__in=self.model.objects.values('document__id')
)
def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try:
document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist:
@@ -80,38 +81,29 @@ class DocumentCheckoutManager(models.Manager):
raise DocumentNotCheckedOut
def get_document_checkout_state(self, document):
if self.is_document_checked_out(document):
if self.is_document_checked_out(document=document):
return STATE_CHECKED_OUT
else:
return STATE_CHECKED_IN
def get_expired_check_outs(self):
expired_list = Document.objects.filter(
pk__in=self.model.objects.filter(
pk__in=self.filter(
expiration_datetime__lte=now()
).values_list('document__pk', flat=True)
).values('document__id')
)
logger.debug('expired_list: %s', expired_list)
return expired_list
def is_document_checked_out(self, document):
if self.model.objects.filter(document=document):
return True
else:
return False
return self.filter(document=document).exists()
class NewVersionBlockManager(models.Manager):
def block(self, document):
self.get_or_create(document=document)
def is_blocked(self, document):
return self.filter(document=document).exists()
def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try:
document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist:
@@ -119,5 +111,12 @@ class NewVersionBlockManager(models.Manager):
return self.get(document__pk=document.pk)
def is_blocked(self, document):
return self.filter(document=document).exists()
def new_versions_allowed(self, document):
if self.filter(document=document).exists():
raise NewDocumentVersionNotAllowed
def unblock(self, document):
self.filter(document=document).delete()

View File

@@ -4,7 +4,7 @@ import logging
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
@@ -68,37 +68,42 @@ class DocumentCheckout(models.Model):
)
def delete(self, *args, **kwargs):
# TODO: enclose in transaction
NewVersionBlock.objects.unblock(self.document)
super(DocumentCheckout, self).delete(*args, **kwargs)
with transaction.atomic():
NewVersionBlock.objects.unblock(document=self.document)
super(DocumentCheckout, self).delete(*args, **kwargs)
def get_absolute_url(self):
return reverse('checkout:checkout_info', args=(self.document.pk,))
return reverse(
viewname='checkout:checkout_info',
kwargs={'document_id': self.document.pk}
)
def natural_key(self):
return self.document.natural_key()
natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs):
# TODO: enclose in transaction
new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut
result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout:
event_document_check_out.commit(
actor=self.user, target=self.document
)
if self.block_new_version:
NewVersionBlock.objects.block(self.document)
logger.info(
'Document "%s" checked out by user "%s"',
self.document, self.user
raise DocumentAlreadyCheckedOut(
_('Document already checked out.')
)
return result
with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout:
event_document_check_out.commit(
actor=self.user, target=self.document
)
if self.block_new_version:
NewVersionBlock.objects.block(self.document)
logger.info(
'Document "%s" checked out by user "%s"',
self.document, self.user
)
return result
class NewVersionBlock(models.Model):

View File

@@ -6,15 +6,15 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts')
permission_document_checkin = namespace.add_permission(
name='checkin_document', label=_('Check in documents')
permission_document_check_in = namespace.add_permission(
label=_('Check in documents'), name='checkin_document'
)
permission_document_checkin_override = namespace.add_permission(
name='checkin_document_override', label=_('Forcefully check in documents')
permission_document_check_in_override = namespace.add_permission(
label=_('Forcefully check in documents'), name='checkin_document_override'
)
permission_document_checkout = namespace.add_permission(
name='checkout_document', label=_('Check out documents')
label=_('Check out documents'), name='checkout_document'
)
permission_document_checkout_detail_view = namespace.add_permission(
name='checkout_detail_view', label=_('Check out details view')
label=_('Check out details view'), name='checkout_detail_view'
)

View File

@@ -5,9 +5,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue
queue_checkouts_periodic = CeleryQueue(
name='checkouts_periodic', label=_('Checkouts periodic'), transient=True
label=_('Checkouts periodic'), name='checkouts_periodic', transient=True
)
queue_checkouts_periodic.add_task_type(
name='mayan.apps.task_check_expired_check_outs',
label=_('Check expired checkouts')
label=_('Check expired checkouts'),
name='mayan.apps.task_check_expired_check_outs'
)

View File

@@ -42,8 +42,8 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
document = Document.objects.get(pk=validated_data.pop('document_pk'))
AccessControlList.objects.check_access(
permissions=permission_document_checkout,
user=self.context['request'].user, obj=document
obj=document, permissions=permission_document_checkout,
user=self.context['request'].user
)
validated_data['document'] = document

View File

@@ -25,7 +25,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def _request_checkedout_document_view(self):
return self.get(
viewname='rest_api:checkedout-document-view',
args=(self.checkout.pk,)
kwargs={'document_pk': self.checkout.pk}
)
def _checkout_document(self):
@@ -44,7 +44,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_checkout_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -52,7 +53,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_document_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -60,14 +61,17 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['document']['uuid'], force_text(self.document.uuid))
self.assertEqual(
response.data['document']['uuid'], force_text(self.document.uuid)
)
def _request_document_checkout_view(self):
return self.post(
@@ -83,7 +87,9 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
self.assertEqual(DocumentCheckout.objects.count(), 0)
def test_document_checkout_with_access(self):
self.grant_access(permission=permission_document_checkout, obj=self.document)
self.grant_access(
obj=self.document, permission=permission_document_checkout
)
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
@@ -102,7 +108,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_document_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -111,7 +117,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_checkout_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -120,10 +127,11 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -23,7 +23,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
@@ -33,29 +33,12 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
)
)
def test_version_creation_blocking(self):
expiration_datetime = now() + datetime.timedelta(days=1)
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)
def test_checkin_in(self):
expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
self.document.check_in()
@@ -72,13 +55,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
with self.assertRaises(DocumentAlreadyCheckedOut):
DocumentCheckout.objects.checkout_document(
document=self.document,
expiration_datetime=expiration_datetime, user=self.admin_user,
expiration_datetime=expiration_datetime, user=self._test_case_user,
block_new_version=True
)
@@ -91,7 +74,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
time.sleep(.11)
@@ -100,18 +83,6 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse(self.document.is_checked_out())
def test_blocking_new_versions(self):
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
NewVersionBlock.objects.block(document=self.document)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)
class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
def test_blocking(self):
@@ -141,3 +112,32 @@ class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse(
NewVersionBlock.objects.is_blocked(document=self.document)
)
def test_blocking_new_versions(self):
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
NewVersionBlock.objects.block(document=self.document)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)
def test_version_creation_blocking(self):
expiration_datetime = now() + datetime.timedelta(days=1)
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self._test_case_user, block_new_version=True
)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)

View File

@@ -8,61 +8,54 @@ from django.utils.timezone import now
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.sources.links import link_upload_version
from mayan.apps.user_management.tests import (
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD,
TEST_USER_USERNAME
)
from ..literals import STATE_CHECKED_OUT, STATE_LABELS
from ..models import DocumentCheckout
from ..permissions import (
permission_document_checkin, permission_document_checkin_override,
permission_document_check_in, permission_document_check_in_override,
permission_document_checkout, permission_document_checkout_detail_view
)
class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
create_test_case_superuser = True
def _checkout_document(self):
expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self._test_case_user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
def _request_document_check_in_view(self):
return self.post(
viewname='checkouts:checkin_document', args=(self.document.pk,),
viewname='checkouts:document_check_in',
kwargs={'document_id': self.document.pk}
)
def test_checkin_document_view_no_permission(self):
self.login_user()
expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
def test_document_check_in_view_no_permission(self):
self._checkout_document()
response = self._request_document_check_in_view()
self.assertEquals(response.status_code, 403)
self.assertEquals(response.status_code, 404)
self.assertTrue(self.document.is_checked_out())
def test_checkin_document_view_with_access(self):
self.login_user()
expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
def test_document_check_in_view_with_access(self):
self._checkout_document()
self.grant_access(
obj=self.document, permission=permission_document_checkin
obj=self.document, permission=permission_document_check_in
)
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_document_check_in_view()
self.assertEquals(response.status_code, 302)
self.assertFalse(self.document.is_checked_out())
self.assertFalse(
DocumentCheckout.objects.is_document_checked_out(
@@ -72,7 +65,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
def _request_document_checkout_view(self):
return self.post(
viewname='checkouts:checkout_document', args=(self.document.pk,),
viewname='checkouts:document_checkout',
kwargs={'document_id': self.document.pk},
data={
'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
@@ -81,14 +75,11 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
)
def test_checkout_document_view_no_permission(self):
self.login_user()
response = self._request_document_checkout_view()
self.assertEquals(response.status_code, 403)
self.assertEquals(response.status_code, 404)
self.assertFalse(self.document.is_checked_out())
def test_checkout_document_view_with_access(self):
self.login_user()
self.grant_access(
obj=self.document, permission=permission_document_checkout
)
@@ -96,11 +87,37 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_document_checkout_view()
self.assertEquals(response.status_code, 302)
self.assertTrue(self.document.is_checked_out())
def _request_checkout_detail_view(self):
return self.get(
viewname='checkouts:checkout_info', args=(self.document.pk,),
)
def test_checkout_detail_view_no_permission(self):
self._checkout_document()
response = self._request_checkout_detail_view()
self.assertNotContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=403
)
def test_checkout_detail_view_with_access(self):
self._checkout_document()
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkout_detail_view()
self.assertContains(response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200)
def test_document_new_version_after_checkout(self):
"""
Gitlab issue #231
@@ -111,31 +128,23 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
- Link to upload version view should not resolve
- Upload version view should reject request
"""
self.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
self.login_superuser()
expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
self._checkout_document()
response = self.post(
'sources:upload_version', args=(self.document.pk,),
viewname='sources:upload_version',
kwargs={'document_id': self.document.pk},
follow=True
)
self.assertContains(
response, text='blocked from uploading',
status_code=200
)
response = self.get(
'documents:document_version_list', args=(self.document.pk,),
viewname='documents:document_version_list',
kwargs={'document_id': self.document.pk},
follow=True
)
@@ -159,26 +168,22 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_superuser, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
self.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
self.grant_access(
obj=self.document, permission=permission_document_check_in
)
self.role.permissions.add(
permission_document_checkin.stored_permission
self.grant_access(
obj=self.document, permission=permission_document_checkout
)
self.role.permissions.add(
permission_document_checkout.stored_permission
)
response = self.post(
'checkouts:checkin_document', args=(self.document.pk,), follow=True
viewname='checkouts:document_check_in',
kwargs={'document_id': self.document.pk},
follow=True
)
self.assertContains(
response, text='Insufficient permissions', status_code=403
)
@@ -186,33 +191,21 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
self.assertTrue(self.document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self):
expiration_datetime = now() + datetime.timedelta(days=1)
self._checkout_document()
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
self.grant_access(
obj=self.document, permission=permission_document_check_in
)
self.assertTrue(self.document.is_checked_out())
self.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
self.grant_access(
obj=self.document, permission=permission_document_check_in_override
)
self.role.permissions.add(
permission_document_checkin.stored_permission
)
self.role.permissions.add(
permission_document_checkin.stored_permission
)
self.role.permissions.add(
permission_document_checkin_override.stored_permission
)
self.role.permissions.add(
permission_document_checkout_detail_view.stored_permission
self.grant_access(
obj=self.document, permission=permission_document_checkout_detail_view
)
response = self.post(
'checkouts:checkin_document', args=(self.document.pk,), follow=True
viewname='checkouts:document_check_in',
kwargs={'document_id': self.document.pk},
follow=True
)
self.assertContains(

View File

@@ -4,33 +4,45 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import (
CheckoutDetailView, CheckoutDocumentView, CheckoutListView,
DocumentCheckinView
DocumentCheckinView, DocumentCheckoutView, DocumentCheckoutDetailView,
DocumentCheckoutListView
)
urlpatterns = [
url(r'^list/$', CheckoutListView.as_view(), name='checkout_list'),
url(
r'^(?P<pk>\d+)/check/out/$', CheckoutDocumentView.as_view(),
name='checkout_document'
regex=r'^documents/$', name='document_checkout_list',
view=DocumentCheckoutListView.as_view()
),
url(
r'^(?P<pk>\d+)/check/in/$', DocumentCheckinView.as_view(),
name='checkin_document'
regex=r'^documents/(?P<document_id>\d+)/check_in/$',
name='document_check_in', view=DocumentCheckinView.as_view()
),
url(
r'^(?P<pk>\d+)/check/info/$', CheckoutDetailView.as_view(),
name='checkout_info'
regex=r'^documents/multiple/check_in/$',
name='document_multiple_check_in', view=DocumentCheckinView.as_view()
),
url(
regex=r'^documents/(?P<document_id>\d+)/checkout/$',
name='document_checkout', view=DocumentCheckoutView.as_view()
),
url(
regex=r'^documents/multiple/checkout/$',
name='document_multiple_checkout', view=DocumentCheckoutView.as_view()
),
url(
regex=r'^documents/(?P<document_id>\d+)/checkout/info/$',
name='document_checkout_info', view=DocumentCheckoutDetailView.as_view()
),
]
api_urls = [
url(
r'^checkouts/$', APICheckedoutDocumentListView.as_view(),
name='checkout-document-list'
regex=r'^checkouts/$', name='checkout-document-list',
view=APICheckedoutDocumentListView.as_view()
),
url(
r'^checkouts/(?P<pk>[0-9]+)/checkout_info/$', APICheckedoutDocumentView.as_view(),
name='checkedout-document-view'
regex=r'^checkouts/(?P<document_id>\d+)/checkout_info/$',
name='checkedout-document-view',
view=APICheckedoutDocumentView.as_view()
),
]

View File

@@ -1,85 +1,154 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm
from .icons import icon_checkout_info
from .models import DocumentCheckout
from .permissions import (
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout, permission_document_checkout_detail_view
permission_document_check_in, permission_document_checkout,
permission_document_checkout_detail_view
)
class CheckoutDocumentView(SingleObjectCreateView):
form_class = DocumentCheckoutForm
class DocumentCheckinView(MultipleObjectConfirmActionView):
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
model = Document
object_permission = permission_document_check_in
pk_url_kwarg = 'document_id'
success_message_singular = '%(count)d document checked in.'
success_message_plural = '%(count)d documents checked in.'
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_extra_context(self):
queryset = self.get_object_list()
AccessControlList.objects.check_access(
permissions=permission_document_checkout, user=request.user,
obj=self.document
)
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
return super(
CheckoutDocumentView, self
).dispatch(request, *args, **kwargs)
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
def form_valid(self, form):
try:
instance = form.save(commit=False)
instance.user = self.request.user
instance.document = self.document
instance.save()
except DocumentAlreadyCheckedOut:
messages.error(self.request, _('Document already checked out.'))
except Exception as exception:
messages.error(
self.request,
_('Error trying to check out document; %s') % exception
return result
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'document_id': self.action_id_list[0]}
)
else:
messages.success(
self.request,
_('Document "%s" checked out successfully.') % self.document
super(DocumentCheckinView, self).get_post_action_redirect()
def object_action(self, form, instance):
DocumentCheckout.objects.check_in_document(
document=instance, user=self.request.user
)
class DocumentCheckoutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckoutForm
model = Document
object_permission = permission_document_checkout
pk_url_kwarg = 'document_id'
success_message_singular = '%(count)d document checked out.'
success_message_plural = '%(count)d documents checked out.'
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
singular='Checkout %(count)d document',
plural='Checkout %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check out document: %s'
) % queryset.first()
}
)
return HttpResponseRedirect(self.get_success_url())
return result
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'document_id': self.action_id_list[0]}
)
else:
super(DocumentCheckoutView, self).get_post_action_redirect()
def object_action(self, form, instance):
DocumentCheckout.objects.checkout_document(
block_new_version=form.cleaned_data['block_new_version'],
document=instance,
expiration_datetime=form.cleaned_data['expiration_datetime'],
user=self.request.user,
)
class DocumentCheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_checkout_detail_view
def get_extra_context(self):
return {
'object': self.document,
'title': _('Check out document: %s') % self.document
'object': self.get_object(),
'title': _(
'Check out details for document: %s'
) % self.get_object()
}
def get_post_action_redirect(self):
return reverse('checkouts:checkout_info', args=(self.document.pk,))
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['document_id'])
class CheckoutListView(DocumentListView):
class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.filter_by_access(
return AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view,
user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
def get_extra_context(self):
context = super(CheckoutListView, self).get_extra_context()
context = super(DocumentCheckoutListView, self).get_extra_context()
context.update(
{
'extra_columns': (
@@ -113,76 +182,3 @@ class CheckoutListView(DocumentListView):
}
)
return context
class CheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_checkout_detail_view
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _(
'Check out details for document: %s'
) % self.get_object()
}
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
class DocumentCheckinView(ConfirmView):
def get_extra_context(self):
document = self.get_object()
context = {
'object': document,
}
if document.get_checkout_info().user != self.request.user:
context['title'] = _(
'You didn\'t originally checked out this document. '
'Forcefully check in the document: %s?'
) % document
else:
context['title'] = _('Check in the document: %s?') % document
return context
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
return reverse('checkouts:checkout_info', args=(self.get_object().pk,))
def view_action(self):
document = self.get_object()
if document.get_checkout_info().user == self.request.user:
AccessControlList.objects.check_access(
permissions=permission_document_checkin,
user=self.request.user, obj=document
)
else:
AccessControlList.objects.check_access(
permissions=permission_document_checkin_override,
user=self.request.user, obj=document
)
try:
document.check_in(user=self.request.user)
except DocumentNotCheckedOut:
messages.error(
self.request, _('Document has not been checked out.')
)
except Exception as exception:
messages.error(
self.request,
_('Error trying to check in document; %s') % exception
)
else:
messages.success(
self.request,
_('Document "%s" checked in successfully.') % document
)

View File

@@ -2,42 +2,42 @@ from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import generics
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .classes import Template
from .serializers import ContentTypeSerializer, TemplateSerializer
class APIContentTypeList(generics.ListAPIView):
class ContentTypeAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns a list of all the available content types.
list:
Return a list of all the available content types.
retrieve:
Return the given content type details.
"""
serializer_class = ContentTypeSerializer
lookup_url_kwarg = 'content_type_id'
queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = ContentTypeSerializer
class APITemplateListView(generics.ListAPIView):
class TemplateAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns a list of partial templates.
get: Returns a list of partial templates.
list:
Return a list of partial templates.
retrieve:
Return the given partial template details.
"""
serializer_class = TemplateSerializer
lookup_url_kwarg = 'template_name'
permission_classes = (IsAuthenticated,)
serializer_class = TemplateSerializer
def get_object(self):
return Template.get(name=self.kwargs['template_name']).render(
request=self.request
)
def get_queryset(self):
return Template.all(rendered=True, request=self.request)
class APITemplateView(generics.RetrieveAPIView):
"""
Returns the selected partial template details.
get: Retrieve the details of the partial template.
"""
serializer_class = TemplateSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return Template.get(name=self.kwargs['name']).render(
request=self.request
)

View File

@@ -4,6 +4,8 @@ import logging
import os
import warnings
from datetime import timedelta
import sys
import traceback
from kombu import Exchange, Queue
@@ -41,6 +43,7 @@ from .settings import (
from .signals import pre_initial_setup, pre_upgrade
from .tasks import task_delete_stale_uploads # NOQA - Force task registration
from .utils import check_for_sqlite
from .warnings import DatabaseWarning
logger = logging.getLogger(__name__)
@@ -74,6 +77,8 @@ class MayanAppConfig(apps.AppConfig):
'Import time error when running AppConfig.ready() of app '
'"%s".', self.name
)
exc_info = sys.exc_info()
traceback.print_exception(*exc_info)
raise exception
@@ -88,7 +93,9 @@ class CommonApp(MayanAppConfig):
def ready(self):
super(CommonApp, self).ready()
if check_for_sqlite():
warnings.warn(force_text(MESSAGE_SQLITE_WARNING))
warnings.warn(
category=DatabaseWarning, message=force_text(MESSAGE_SQLITE_WARNING)
)
Template(
name='menu_main', template_name='appearance/menu_main.html'

View File

@@ -72,16 +72,6 @@ class ErrorLogNamespace(object):
return ErrorLogEntry.objects.filter(namespace=self.name)
class FakeStorageSubclass(object):
"""
Placeholder class to allow serializing the real storage subclass to
support migrations.
"""
def __eq__(self, other):
return True
class MissingItem(object):
_registry = []
@@ -302,7 +292,7 @@ class Template(object):
def get_absolute_url(self):
return reverse(
viewname='rest_api:template-detail', kwargs={'template_pk': self.name}
viewname='rest_api:template-detail', kwargs={'template_name': self.name}
)
def render(self, request):
@@ -315,10 +305,10 @@ class Template(object):
context=context,
).render()
content = result.rendered_content.replace('\n', '')
self.html = content
self.hex_hash = hashlib.sha256(content).hexdigest()
# Calculate the hash of the bytes version but return the unicode
# version
self.html = result.rendered_content.replace('\n', '')
self.hex_hash = hashlib.sha256(result.content).hexdigest()
return self

View File

@@ -4,6 +4,7 @@ import os
from django import forms
from django.conf import settings
from django.contrib.admin.utils import label_for_field
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.module_loading import import_string
@@ -13,7 +14,7 @@ from mayan.apps.acls.models import AccessControlList
from .classes import Package
from .models import UserLocaleProfile
from .utils import resolve_attribute
from .utils import introspect_attribute, resolve_attribute
from .widgets import DisableableSelectWidget, PlainWidget, TextAreaDiv
@@ -38,29 +39,94 @@ class ChoiceForm(forms.Form):
}
)
selection = forms.MultipleChoiceField(widget=DisableableSelectWidget())
selection = forms.MultipleChoiceField(
required=False, widget=DisableableSelectWidget()
)
class FormOptions(object):
def __init__(self, form, kwargs, options=None):
"""
Option definitions will be iterated. The option value will be
determined in the following order: as passed via keyword
arguments during form intialization, as form get_... method or
finally as static Meta options. This is to allow a form with
Meta options or method to be overrided at initialization
and increase the usability of a single class.
"""
for option_definition in self.option_definitions:
name = option_definition.keys()[0]
default_value = option_definition.values()[0]
try:
# Check for a runtime value via kwargs
value = kwargs.pop(name)
except KeyError:
try:
# Check if there is a get_... method
value = getattr(self, 'get_{}'.format(name))()
except AttributeError:
try:
# Check the meta class options
value = getattr(options, name)
except AttributeError:
value = default_value
setattr(self, name, value)
class DetailFormOption(FormOptions):
# Dictionary list of option names and default values
option_definitions = (
{'extra_fields': []},
)
class DetailForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.extra_fields = kwargs.pop('extra_fields', ())
self.opts = DetailFormOption(
form=self, kwargs=kwargs, options=getattr(self, 'Meta', None)
)
super(DetailForm, self).__init__(*args, **kwargs)
for extra_field in self.extra_fields:
result = resolve_attribute(obj=self.instance, attribute=extra_field['field'])
label = 'label' in extra_field and extra_field['label'] or None
for extra_field in self.opts.extra_fields:
obj = extra_field.get('object', self.instance)
field = extra_field['field']
result = resolve_attribute(
attribute=field, obj=obj
)
label = extra_field.get('label', None)
if not label:
attribute_name, obj = introspect_attribute(
attribute_name=field, obj=obj
)
if not obj:
label = _('None')
else:
try:
label = getattr(
getattr(obj, attribute_name), 'short_description'
)
except AttributeError:
label = label_for_field(
name=attribute_name, model=obj
)
# TODO: Add others result types <=> Field types
if isinstance(result, models.query.QuerySet):
self.fields[extra_field['field']] = \
forms.ModelMultipleChoiceField(
queryset=result, label=label)
self.fields[field] = forms.ModelMultipleChoiceField(
queryset=result, label=label
)
else:
self.fields[extra_field['field']] = forms.CharField(
label=extra_field['label'],
self.fields[field] = forms.CharField(
initial=resolve_attribute(
obj=self.instance,
attribute=extra_field['field']
),
obj=obj,
attribute=field
), label=label,
widget=extra_field.get('widget', PlainWidget)
)
@@ -131,7 +197,7 @@ class FileDisplayForm(forms.Form):
self.fields['text'].initial = file_object.read()
class FilteredSelectionFormOptions(object):
class FilteredSelectionFormOptions(FormOptions):
# Dictionary list of option names and default values
option_definitions = (
{'allow_multiple': False},
@@ -141,40 +207,12 @@ class FilteredSelectionFormOptions(object):
{'model': None},
{'permission': None},
{'queryset': None},
{'required': True},
{'user': None},
{'widget_class': None},
{'widget_attributes': {'size': '10'}},
)
def __init__(self, form, kwargs, options=None):
"""
Option definitions will be iterated. The option value will be
determined in the following order: as passed via keyword
arguments during form intialization, as form get_... method or
finally as static Meta options. This is to allow a form with
Meta options or method to be overrided at initialization
and increase the usability of a single class.
"""
for option_definition in self.option_definitions:
name = option_definition.keys()[0]
default_value = option_definition.values()[0]
try:
# Check for a runtime value via kwargs
value = kwargs.pop(name)
except KeyError:
try:
# Check if there is a get_... method
value = getattr(self, 'get_{}'.format(name))()
except AttributeError:
try:
# Check the meta class options
value = getattr(options, name)
except AttributeError:
value = default_value
setattr(self, name, value)
class FilteredSelectionForm(forms.Form):
"""
@@ -191,7 +229,7 @@ class FilteredSelectionForm(forms.Form):
raise ImproperlyConfigured(
'{} requires a queryset or a model to be specified as '
'a meta option or passed during initialization.'.format(
self.__class__
self.__class__.__name__
)
)
@@ -211,17 +249,17 @@ class FilteredSelectionForm(forms.Form):
else:
widget_class = opts.widget_class
super(FilteredSelectionForm, self).__init__(*args, **kwargs)
if opts.permission:
queryset = AccessControlList.objects.filter_by_access(
queryset = AccessControlList.objects.restrict_queryset(
permission=opts.permission, queryset=queryset,
user=opts.user
)
super(FilteredSelectionForm, self).__init__(*args, **kwargs)
self.fields[opts.field_name] = field_class(
help_text=opts.help_text, label=opts.label,
queryset=queryset, required=True,
queryset=queryset, required=opts.required,
widget=widget_class(attrs=opts.widget_attributes),
**extra_kwargs
)

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,12 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_about = Icon(driver_name='fontawesome', symbol='info')
icon_add_all = Icon(
driver_name='fontawesome-layers', data=[
{'class': 'far fa-circle'},
{'class': 'fas fa-plus', 'transform': 'shrink-6'}
]
)
icon_assign_remove_add = Icon(driver_name='fontawesome', symbol='plus')
icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus')
icon_check_version = Icon(driver_name='fontawesome', symbol='sync')
@@ -43,6 +49,12 @@ icon_ok = Icon(
icon_packages_licenses = Icon(
driver_name='fontawesome', symbol='certificate'
)
icon_remove_all = Icon(
driver_name='fontawesome-layers', data=[
{'class': 'far fa-circle'},
{'class': 'fas fa-minus', 'transform': 'shrink-6'}
]
)
icon_setup = Icon(
driver_name='fontawesome', symbol='cog'
)

View File

@@ -57,12 +57,12 @@ link_documentation = Link(
link_object_error_list = Link(
icon_class=icon_object_error_list,
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Errors'),
permission=permission_error_log_view, text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'),
permission=permission_error_log_view, text=_('Clear all'),
view='common:object_error_list_clear',
)
link_forum = Link(

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