Compare commits

...

175 Commits

Author SHA1 Message Date
Roberto Rosario
6d397a648a Merge branch 'versions/minor' into features/redactions_upstream_merge
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-13 19:58:46 -04:00
Roberto Rosario
4dd270e75b Add mixins to retrieve content type object
Add ContentTypeViewMixin and ExternalContentTypeObjectMixin.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-05 00:27:15 -04:00
Roberto Rosario
3428c6aa20 Update ExternalObjectMixin
Call ModelPermission to select the proper manager for the queryset
when specifying just the model.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-05 00:23:45 -04:00
Roberto Rosario
eb1fb8511b Move manager get code to ModelPermission class
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-05 00:20:06 -04:00
Roberto Rosario
bdbc7ef086 Add rectangle drawing transformations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-31 01:55:58 -04:00
Roberto Rosario
abea863184 Fix metadata widget overflow on long values
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-31 01:55:24 -04:00
Roberto Rosario
d394583729 Remove HTML title anchor on disabled pages
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-31 01:54:51 -04:00
Roberto Rosario
4db59c0808 Disable page links on disabled pages
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-31 01:54:21 -04:00
Roberto Rosario
12f24316a1 Improve page navigation limit logic
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-31 01:53:35 -04:00
Roberto Rosario
ef0843276b Support source column widget condition
- Add default HTML anchor widget for source columns that
  return and absolute URL.
- Fix CSS pointer behavior on list item panel headers.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-31 01:51:43 -04:00
Roberto Rosario
e20102333e Update URLs for uniformity
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-30 04:46:21 -04:00
Roberto Rosario
4ecf075fd4 Add support for disabling document pages
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-30 03:11:20 -04:00
Roberto Rosario
cc81a6905a Add kwargs
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-30 03:10:25 -04:00
Roberto Rosario
3c9454160f Support custom model managers for check_access()
Allow app to specify which model manager will be used
when creating the queryset that is passed to check_access.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-30 03:10:15 -04:00
Roberto Rosario
2e1600c334 Remove obsolete DocumentPageCachedImage manager
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-29 02:53:07 -04:00
Roberto Rosario
3e9f30cb91 Reduce the number of pager buttons
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-29 02:44:31 -04:00
Roberto Rosario
a3a78f755d Display thousand commas in numeric dashboard
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-29 02:36:57 -04:00
Roberto Rosario
3988dedebf Add missing migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-28 22:36:51 -04:00
Roberto Rosario
ff34c7d00a Add cabinet label help text
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-28 22:36:17 -04:00
Roberto Rosario
fe2de33e98 Display column help text as a tooltip
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-28 22:35:52 -04:00
Roberto Rosario
3efd1bd89d Add web links app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-27 01:08:54 -04:00
Roberto Rosario
ea516cbc23 Correct order of super in file caching test mixin
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-27 00:56:40 -04:00
Roberto Rosario
52ad3e7418 Update the URL class to work with Python 3
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 23:23:18 -04:00
Roberto Rosario
a001b4bbb3 Move new version block to its own test case
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 18:20:23 -04:00
Roberto Rosario
31ed0e1ac8 Clean non ASCII character in docstring
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 18:19:56 -04:00
Roberto Rosario
9ad82695d9 Add cleaning up of Python 3 files
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 18:19:38 -04:00
Roberto Rosario
69af4dd6b3 PEP8 cleanups
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 18:03:13 -04:00
Roberto Rosario
1c7ceca432 Add file caching tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 15:49:07 -04:00
Roberto Rosario
c05dcf5b05 Remove fs_cleanup file_descriptor argument
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 15:16:54 -04:00
Roberto Rosario
85b05dd6ec Add kwargs to fs_cleanup usage
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 15:16:37 -04:00
Roberto Rosario
9752584135 Rename file_descriptor usage to file_object
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 15:14:53 -04:00
Roberto Rosario
fd0d5728a1 Improve toolbar display logic
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 02:34:01 -04:00
Roberto Rosario
88863fd6d0 Fix typo in Cache get_model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 02:23:09 -04:00
Roberto Rosario
3a7025d9c4 Add exists method to cache file model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 02:22:50 -04:00
Roberto Rosario
150c5d8cc2 Make cache columns sortable
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 02:22:34 -04:00
Roberto Rosario
93ba547350 Convert workflow previews app to use file caching
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 02:22:04 -04:00
Roberto Rosario
f920dffc01 Remove document model cache invalidation
The cache invalidation is now handled by the file caching app.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 01:33:41 -04:00
Roberto Rosario
c2e99e6efb Purge cache partition before deleting them
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 01:33:14 -04:00
Roberto Rosario
ff6674cc4a Fix workflow preview under Python 3
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 01:24:55 -04:00
Roberto Rosario
669dfeb30a Use common app serialization util
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-26 01:21:01 -04:00
Roberto Rosario
6635bb4235 Tweak CSS to unify widths in plain template
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-25 20:36:47 -04:00
Roberto Rosario
88bc29e4d7 Update the file caching app
- Add view to list available caches.
- Add links to view and purge caches.
- Add permissions.
- Add events.
- Add purge task.
- Remove document image clear link and view.
  This is now handled by the file caching app.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-25 02:24:33 -04:00
Roberto Rosario
9315776926 Add missing migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-25 00:52:21 -04:00
Roberto Rosario
40a306996c Update transformation tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-25 00:48:47 -04:00
Roberto Rosario
033cecd946 Move pagination navigation inside the toolbar
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-25 00:44:07 -04:00
Roberto Rosario
ee63829e7f Update runserver targets to run nonthreaded
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 16:07:28 -04:00
Roberto Rosario
e4bc007bba Unify lists header markup
Convert list headers into a separate template

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 16:06:45 -04:00
Roberto Rosario
84b329f661 Fix more test case method resolution
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 15:29:25 -04:00
Roberto Rosario
4c73239dde Fix http.URL class final URL generation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 03:20:57 -04:00
Roberto Rosario
2e12a6af41 Fix test case method resolution
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 02:58:29 -04:00
Roberto Rosario
3d7e6b6fbe Update GUID to GID in documentation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 02:50:55 -04:00
Roberto Rosario
6f907d156a Remove task inspection from task manager app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 02:49:37 -04:00
Roberto Rosario
fac77a2f73 Workaround for the OCR task-inside-task issue
Thanks to Jakob Haufe (@sur5r) for the solution.
2423254aa4

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 02:25:49 -04:00
Jiri B
0c3b6e5388 I was shocked my PDFs were deleted from source directory thus this needs to be clarified. 2019-07-24 02:21:01 -04:00
Roberto Rosario
e652c7208c Move Celery dependencies to task_manager app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 02:17:37 -04:00
Roberto Rosario
53928b2ab6 Run EXIFTOOL always regardless of MIME type
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 01:57:20 -04:00
Roberto Rosario
afc6b54520 Update release notes and changelog
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-24 01:56:09 -04:00
Roberto Rosario
070355033e Update changelog
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:41:44 -04:00
Roberto Rosario
0029d3074f Modify PYTHONPATH in-place
Avoid including a hard coded path.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:40:10 -04:00
Roberto Rosario
4558894faf Include devpi-server as a development dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:39:42 -04:00
Roberto Rosario
adeea6247f Update Docker stack file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:38:48 -04:00
Roberto Rosario
3563297d48 Update default Docker compose file
- Launch a Redis container.
- Include optional services examples.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:34:58 -04:00
Roberto Rosario
1e1b4dedf4 Update Docker make file
- Include PIP proxies.
- Add docker compose targets.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:22:30 -04:00
Roberto Rosario
d65bbb718a Update Docker entrypoint
- Use bash instead of sh/dash to support argument slicing.
- Default Celery worker concurrency to 0 (auto).
- Set DJANGO_SETTINGS_MODULE environment variable to make it
  available to sub processes.
- Add entrypoint commands to run single workers, single gunicorn
  or single celery commands like "flower".
- Update Gunicorn to use sync workers.
- Add platform template to return queues for a worker.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:15:12 -04:00
Roberto Rosario
5352c6ac6f Update Docker image
- Remove Redis from the Docker image.
- Add Celery flower.
- Add Python 3 packages needed for in-container pip installs.
- Fix typos.
- Allow PIP proxying.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-23 21:12:11 -04:00
Roberto Rosario
cb7d5bf82a Update djcelery imports
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-20 00:15:19 -04:00
Roberto Rosario
41a7d00e9e Fix setting typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-19 20:05:12 -04:00
Roberto Rosario
82d7339a64 Update documentation Docker chapter
Update to show the new MAYAN_DATABASES setting. Remove
settings that are not Docker exclusive.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-19 20:04:21 -04:00
Roberto Rosario
e889021f43 Update command options
Match the rename of the installjavascript command rename
to installdependencies.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-19 20:02:40 -04:00
Roberto Rosario
d3a53fb53a Remove unused SETTING_FILE_TEMPLATE
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-19 20:02:00 -04:00
Roberto Rosario
b6565effb5 Support wildcard MIME type associations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-19 01:04:04 -04:00
Roberto Rosario
cf43ef2f73 Fix setting import
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 05:19:40 -04:00
Roberto Rosario
6ca2845d19 Update requirement files
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 04:44:00 -04:00
Roberto Rosario
ab601f9180 Initial commit to support Celery 4.3.0
Merges 55e9b2263c from versions/next
with code from GitLab issue #594 and GitLab merge request !55.

Thanks to Jakob Haufe (@sur5r) and Jesaja Everling (@jeverling)
for much of the research and code updates.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 04:30:11 -04:00
Roberto Rosario
0b42567179 Remove direct Celery queue update
Queue updates are handled by the task manager app.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 02:41:00 -04:00
Roberto Rosario
030ddd68e0 PEP8 cleanups
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 01:13:00 -04:00
Roberto Rosario
649571ebb1 Add kwargs
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 00:48:22 -04:00
Roberto Rosario
b99bb88008 Update OCR manager to use document cache
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-17 00:47:28 -04:00
Roberto Rosario
fd08a23339 Soften top bar shadow
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-16 16:21:24 -04:00
Roberto Rosario
917ec55ada Style tweaks
Enable dashboard widget icon shadows. Make block button text
shadow more pronounced.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-16 16:18:36 -04:00
Roberto Rosario
ec4644b5c9 Fix typo on open method
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-16 01:28:55 -04:00
Roberto Rosario
ff86c4c518 Unbind non applicable workflow runtime proxy links
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-16 01:28:31 -04:00
Roberto Rosario
daebf2ddcc Don't react on paginator current page click
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-16 01:27:44 -04:00
Roberto Rosario
49a16acdf5 Backport panel selection and panel toolbar
Support selection by panel body clicking. Styling changes for highlighted panel.
Self-display multiple item action list. New select all button.
Fix fancybox click area on thumbnail display.
Remove the multi item form processing view.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-16 01:24:57 -04:00
Roberto Rosario
8c064c953a Add file caching app
Convert document image cache to use file cache manager app.
Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-15 01:33:32 -04:00
Roberto Rosario
3c7a23a5a9 Add support for setting post update callbacks
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-15 01:24:22 -04:00
Roberto Rosario
6bcf35bef5 Add database conversion removal explanation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 05:17:15 -04:00
Roberto Rosario
7ef6102876 Update release notes
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:52:34 -04:00
Roberto Rosario
4363bba0fe Remove encapsulate
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:50:37 -04:00
Roberto Rosario
e2f2181ebb Complete multiple check in/out support
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:49:39 -04:00
Roberto Rosario
d4f7e2cd16 Support creating multiple test users
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:49:09 -04:00
Roberto Rosario
058e36b4a9 Introspect proxy's parent only it is a model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:48:00 -04:00
Roberto Rosario
1ddd5f26b1 Support menu inheritance
Proxy models will now inherit the menus from their parents.
Added to allow checked out documents to show multi item links
of their parents.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:40:48 -04:00
Roberto Rosario
44652d49fb Add test utility to return an id_list
Makes creating an id_list for testing from a list test instances
easier.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:39:48 -04:00
Roberto Rosario
119c1bde76 Add user test mixin to base test class
Allow tests to create test users.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:39:18 -04:00
Roberto Rosario
ed227b4111 Emphasize source column labels
Use the same CSS style as the view's extra_columns.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-12 04:38:06 -04:00
Roberto Rosario
c44090aca6 Initial commit to support multidocument checkouts
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 20:00:17 -04:00
Roberto Rosario
8a7da6a103 Update release notes closed issues
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 02:26:24 -04:00
Roberto Rosario
3e3b1f75a0 Remove django-environ
Work done in 9564db398f

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 02:02:45 -04:00
Roberto Rosario
1ab7b7b9b1 Backport FakeStorageSubclass from versions/next
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:56:06 -04:00
Roberto Rosario
3fab5c1427 Return empty dict if there is no config file
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:31:37 -04:00
Roberto Rosario
516c3aeb2c Add default for OCR backend argument setting
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:31:05 -04:00
Roberto Rosario
3ac1000b46 Merge remote-tracking branch 'origin/features/move_django_settings' into merge_features 2019-07-11 01:21:40 -04:00
Roberto Rosario
4adeefc978 Merge remote-tracking branch 'origin/features/move_yaml_code' into merge_features
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-11 01:21:23 -04:00
Roberto Rosario
8bc4b6a95e Move YAML code to its own module
Code now resides in common.serialization in the form
of two new functions: yaml_load and yaml_dump.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-10 19:35:42 -04:00
Roberto Rosario
37e85590e8 Move Django and Celery settings
Django settings now reside in the smart settings app.
Celery settings now reside in the task manager app.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-10 19:02:22 -04:00
Roberto Rosario
78a0189e1c Add YAML env variables support to platform app
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-10 00:34:09 -04:00
Roberto Rosario
91b0b2d9c3 Update smart setting's app URLs for uniformity
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:46:09 -04:00
Roberto Rosario
8a54deba3d Unify individual database configuration options
All database configuration is now done using MAYAN_DATABASES to
mirror Django way of doing database setup.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:45:30 -04:00
Roberto Rosario
22da1e0a78 Update import
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:43:39 -04:00
Roberto Rosario
c9668d62e5 Move mailer defaults to the literals module
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:43:15 -04:00
Roberto Rosario
7a01a77c43 Remove smart_settings * import
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:42:57 -04:00
Roberto Rosario
9564db398f Backport configuration file improvements
Remove support for quoted entried. Support unquoted entries. Support
custom location for the config files.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-09 15:40:20 -04:00
Roberto Rosario
7faa24eb7b Remove database conversion command
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:42:11 -04:00
Roberto Rosario
51f278301b Sort list of apps
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:40:24 -04:00
Roberto Rosario
2cc35c3c61 Remove outdated contrib scripts
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:37:58 -04:00
Roberto Rosario
8c73fda1ae Rename installjavascript to installdependencies
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:35:14 -04:00
Roberto Rosario
8811c8269f Rename document states apps view and URLs.
Object layout: WorkflowTemplate, WorkflowInstance, WorkflowRuntimeProxy,
WorkflowTemplateState, WorkflowTemplateTransition.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 02:21:58 -04:00
Roberto Rosario
f36f99c5fb Split workflow URL patterns
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:23:49 -04:00
Roberto Rosario
0e972eff06 Fix typos and PEP8 warnings
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:12:25 -04:00
Roberto Rosario
7913b5ddcc Sort dictionary entry
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:06:58 -04:00
Roberto Rosario
1c86ea5b5b Backport individual index rebuild support
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 01:03:39 -04:00
Roberto Rosario
ec6a3bd960 Move AJAX spinner to the left of the top bar
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:43:14 -04:00
Roberto Rosario
080553c797 Add trashed date time label and position
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:38:47 -04:00
Roberto Rosario
08ee07e652 Remove duplicated trashed document previews
Side effect of source column inheritance added in
06c3ef6583.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:37:47 -04:00
Roberto Rosario
d7d77fcb55 Backport workflow email action
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-07 00:27:29 -04:00
Roberto Rosario
bb5324ef50 Encode settings YAML before hashing
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
2019-07-06 17:14:44 -04:00
Roberto Rosario
4c212f6ea4 Backport workflow context and field support
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:13:26 -04:00
Roberto Rosario
941356ed69 Add a general use YAML validator
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:11:43 -04:00
Roberto Rosario
97804b255b Add and exclude Index instance columns
Exclude inherited columns from the Index models.
Add the label columns to Index instances.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:10:41 -04:00
Roberto Rosario
06c3ef6583 Add source column inheritance and exclusions
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 04:09:44 -04:00
Roberto Rosario
6cd857e2bf Use Select2 widget for the document type selection form
This was committed in 109fcba795 without
adding the actual change.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 02:44:00 -04:00
Roberto Rosario
fbb0f0b9bd Backport workflow preview refactor
GitLab issue #532.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 02:41:16 -04:00
Roberto Rosario
9e068c3e83 Add topbar shadow
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 02:01:48 -04:00
Roberto Rosario
72a3807354 Add vertical main menu
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-07-06 01:53:45 -04:00
Roberto Rosario
109fcba795 Use Select2 for the document type selection form
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 23:26:11 -04:00
Roberto Rosario
01380e0572 Merge branch 'versions/minor' of gitlab.com:mayan-edms/mayan-edms into versions/minor 2019-07-05 23:23:50 -04:00
Roberto Rosario
5146c6d202 Tweak setup buttom border and tag shadows
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 23:23:34 -04:00
Roberto Rosario
300bdbfc8a Tweak setup buttom border and tag shadows
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 21:34:20 -04:00
Roberto Rosario
a0331e0236 Add support for icon shadows
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-07-05 21:26:45 -04:00
Roberto Rosario
00d4989289 Add missing migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 15:11:14 -04:00
Roberto Rosario
42a7ebeea2 Finish redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 14:16:11 -04:00
Roberto Rosario
3d22f48555 Add draw box by percentage
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:13:20 -04:00
Roberto Rosario
488e048d8f Remove old remarks and add redirect
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:12:41 -04:00
Roberto Rosario
2f82559a5c Add verbose name for the Redaction model
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:12:08 -04:00
Roberto Rosario
7d5b7b9fc4 Fix static media folder
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-26 00:11:52 -04:00
Roberto Rosario
7aa68b8bbf Initial commit of the redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 12:13:49 -04:00
Roberto Rosario
aecde926f2 Fix varaible typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 12:08:25 -04:00
Roberto Rosario
6b95628e56 Add rectangle drawing transformation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 10:23:30 -04:00
Roberto Rosario
56a1b97b46 Update changes file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:17:01 -04:00
Roberto Rosario
34a5a54c8b Add sortable index instance label column
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:15:52 -04:00
Roberto Rosario
0c17ab3f8a Improve source column exclusion
Improve for model subclasses in partial querysets.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 09:08:02 -04:00
Roberto Rosario
c967a25f82 Support exclusions from source columns
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-25 00:16:29 -04:00
Roberto Rosario
7562588c42 Fix typo
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:55:02 -04:00
Roberto Rosario
a1a706b7b9 Add link to sort individual indexes
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:50:01 -04:00
Roberto Rosario
d623cb2df5 Sort function
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:04:49 -04:00
Roberto Rosario
488ddcf1e1 Rename CHANGES file
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:04:07 -04:00
Roberto Rosario
3d39893f17 Add columns to show document count per workflow
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 23:03:06 -04:00
Roberto Rosario
3694839d97 Use Select2 for the document type selection form
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 19:29:12 -04:00
Roberto Rosario
cce27aceca Allow client builds
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:42:59 -04:00
Roberto Rosario
c73d251370 Generate metadata by name not label
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:32:14 -04:00
Roberto Rosario
091f0d1cfd Generate new metadata when label is ambiguous
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-24 16:20:56 -04:00
Roberto Rosario
d2affdcf21 Merge branch 'feature/document_importer' into nightly
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 17:39:19 -04:00
Roberto Rosario
885d430b98 Merge branch 'versions/minor' into nightly 2019-06-21 17:38:08 -04:00
Roberto Rosario
39eabe1c54 Associate metadata to all types
Previously metadata types were associated to documents types
if the metadata type was newly created.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 17:37:00 -04:00
Roberto Rosario
f6ad579829 Merge branch 'versions/minor' into nightly
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 12:05:19 -04:00
Roberto Rosario
6fc9e46882 Merge branch 'versions/minor' into feature/document_importer 2019-06-21 11:53:09 -04:00
Roberto Rosario
2d326a679d Merge branch 'master' into feature/document_importer 2019-06-21 11:53:03 -04:00
Roberto Rosario
aa8c2db446 Merge branch 'master' into feature/document_importer
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-06-21 00:06:49 -04:00
Roberto Rosario
925b55d76d Support ignoring certain rows
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-20 10:12:53 -04:00
Roberto Rosario
5808d3653d Add support for ignoring import errors
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-20 10:05:24 -04:00
Roberto Rosario
bc072f7b7e Add column mapping support
Add support for specifying metadata columns.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 17:47:32 -04:00
Roberto Rosario
b3d59eee39 Add MVP of the importer app
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 16:02:00 -04:00
Roberto Rosario
7d379a52af Add a reusable task to upload documents
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 16:00:59 -04:00
Roberto Rosario
499ab1f3e7 Allow disabling the random primary key test mixin
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2019-06-19 15:59:15 -04:00
506 changed files with 32125 additions and 6685 deletions

View File

@@ -63,6 +63,7 @@ job_docker_nightly:
only: only:
- nightly - nightly
- staging - staging
- /^clients\/.+$/
job_documentation_build: job_documentation_build:
stage: build_documentation stage: build_documentation
@@ -160,6 +161,7 @@ job_push_python:
- releases/python - releases/python
- staging - staging
- nightly - nightly
- /^clients\/.+$/
test-mysql: test-mysql:
<<: *test_base <<: *test_base

View File

@@ -115,6 +115,12 @@ source_lang = en
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.po
type = PO type = PO
[mayan-edms.file_caching-3-0]
file_filter = mayan/apps/file_caching/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/file_caching/locale/en/LC_MESSAGES/django.po
type = PO
[mayan-edms.file_metadata-3-0] [mayan-edms.file_metadata-3-0]
file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po
source_lang = en source_lang = en
@@ -222,3 +228,10 @@ file_filter = mayan/apps/user_management/locale/<lang>/LC_MESSAGES/django.po
source_lang = en source_lang = en
source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.po source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.po
type = PO type = PO
[mayan-edms.weblink-3-0]
file_filter = mayan/apps/weblinks/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/weblinks/locale/en/LC_MESSAGES/django.po
type = PO

10
CHANGES_BC.rst Normal file
View File

@@ -0,0 +1,10 @@
- Use Select2 widget for the document type selection form.
- Update source column matching to be additive and not exclusive.
- Add two columns to show the number of documents per workflow and
workflow state.
- Sort module.
- Add link to sort individual indexes.
- Support exclusions from source columns.
- Improve source column exclusion. Improve for model subclasses in partial querysets.
- Add sortable index instance label column.
- Add rectangle drawing transformation.

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ clean-pyc: ## Remove Python artifacts.
find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} + find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing # Testing
@@ -234,10 +234,10 @@ generate-requirements: ## Generate all requirements files from the project deped
# Dev server # Dev server
runserver: ## Run the development server. runserver: ## Run the development server.
./manage.py runserver --settings=mayan.settings.development $(ADDRPORT) ./manage.py runserver --nothreading --settings=mayan.settings.development $(ADDRPORT)
runserver_plus: ## Run the Django extension's development server. runserver_plus: ## Run the Django extension's development server.
./manage.py runserver_plus --settings=mayan.settings.development $(ADDRPORT) ./manage.py runserver_plus --nothreading --settings=mayan.settings.development $(ADDRPORT)
shell_plus: ## Run the shell_plus command. shell_plus: ## Run the shell_plus command.
./manage.py shell_plus --settings=mayan.settings.development ./manage.py shell_plus --settings=mayan.settings.development
@@ -258,7 +258,7 @@ test-with-docker-frontend: ## Launch a front end instance that uses the producti
./manage.py runserver --settings=mayan.settings.staging.docker ./manage.py runserver --settings=mayan.settings.staging.docker
test-with-docker-worker: ## Launch a worker instance that uses the production-like services. test-with-docker-worker: ## Launch a worker instance that uses the production-like services.
./manage.py celery worker --settings=mayan.settings.staging.docker -B -l INFO -O fair DJANGO_SETTINGS_MODULE=mayan.settings.staging.docker ./manage.py celery worker -A mayan -B -l INFO -O fair
docker-mysql-on: ## Launch and initialize a MySQL Docker container. docker-mysql-on: ## Launch and initialize a MySQL Docker container.
docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql docker run -d --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=True -e MYSQL_DATABASE=mayan_edms mysql

View File

@@ -1,72 +0,0 @@
#!/usr/bin/env bash
INSTALLATION_DIRECTORY=/home/vagrant/mayan-edms/
DB_NAME=mayan_edms
DB_PASSWORD=test123
cat << EOF | sudo tee -a /etc/motd.tail
**********************************sudo apt
Mayan EDMS Vagrant Development Box
**********************************
EOF
# Update sources
echo -e "\n -> Running apt-get update & upgrade \n"
sudo apt-get -qq update
sudo apt-get -y upgrade
echo -e "\n -> Installing core binaries \n"
sudo apt-get -y install git-core python-virtualenv gcc python-dev libjpeg-dev libpng-dev libtiff-dev tesseract-ocr poppler-utils libreoffice
echo -e "\n -> Cloning development branch of repository \n"
git clone /mayan-edms-repository/ $INSTALLATION_DIRECTORY
cd $INSTALLATION_DIRECTORY
git checkout development
git reset HEAD --hard
echo -e "\n -> Setting up virtual env \n"
virtualenv venv
source venv/bin/activate
echo -e "\n -> Installing python dependencies \n"
pip install -r requirements.txt
echo -e "\n -> Running Mayan EDMS initial setup \n"
./manage.py initialsetup
echo -e "\n -> Installing Redis server \n"
sudo apt-get install -y redis-server
pip install redis
echo -e "\n -> Installing testing software \n"
pip install coverage
echo -e "\n -> Installing MySQL \n"
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password '$DB_PASSWORD
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password '$DB_PASSWORD
sudo apt-get install -y mysql-server libmysqlclient-dev
# Create a passwordless root and travis users
mysql -u root -p$DB_PASSWORD -e "SET PASSWORD = PASSWORD('');"
mysql -u root -e "CREATE USER 'travis'@'localhost' IDENTIFIED BY '';GRANT ALL PRIVILEGES ON * . * TO 'travis'@'localhost';FLUSH PRIVILEGES;"
mysql -u travis -e "CREATE DATABASE $DB_NAME;"
pip install mysql-python
echo -e "\n -> Installing PostgreSQL \n"
sudo apt-get install -y postgresql postgresql-server-dev-all
sudo -u postgres psql -c 'create database mayan_edms;' -U postgres
sudo cat > /etc/postgresql/9.3/main/pg_hba.conf << EOF
local all postgres trust
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
EOF
pip install -q psycopg2

File diff suppressed because it is too large Load Diff

View File

@@ -1,171 +0,0 @@
#!/usr/bin/env bash
# ====== CONFIG ======
INSTALLATION_DIRECTORY=/usr/share/mayan-edms/
DB_NAME=mayan_edms
DB_USERNAME=mayan
DB_PASSWORD=test123
# ==== END CONFIG ====
cat << EOF | tee -a /etc/motd.tail
**********************************
Mayan EDMS Vagrant Production Box
**********************************
EOF
echo -e "\n -> Running apt-get update & upgrade \n"
apt-get -qq update
apt-get -y upgrade
echo -e "\n -> Installing core binaries \n"
apt-get install nginx supervisor redis-server postgresql libpq-dev libjpeg-dev libmagic1 libpng-dev libreoffice libtiff-dev gcc ghostscript gpgv python-dev python-virtualenv tesseract-ocr poppler-utils -y
echo -e "\n -> Setting up virtualenv \n"
rm -f ${INSTALLATION_DIRECTORY}
virtualenv ${INSTALLATION_DIRECTORY}
source ${INSTALLATION_DIRECTORY}bin/activate
echo -e "\n -> Installing Mayan EDMS from PyPI \n"
pip install mayan-edms
echo -e "\n -> Installing Python client for PostgreSQL, Redis, and uWSGI \n"
pip install psycopg2 redis uwsgi
echo -e "\n -> Creating the database for the installation \n"
echo "CREATE USER mayan WITH PASSWORD '$DB_PASSWORD';" | sudo -u postgres psql
sudo -u postgres createdb -O $DB_USERNAME $DB_NAME
echo -e "\n -> Creating the directories for the logs \n"
mkdir /var/log/mayan
echo -e "\n -> Making a convenience symlink \n"
cd ${INSTALLATION_DIRECTORY}
ln -s lib/python2.7/site-packages/mayan .
echo -e "\n -> Creating an initial settings file \n"
mayan-edms.py createsettings
echo -e "\n -> Updating the mayan/settings/local.py file \n"
cat >> mayan/settings/local.py << EOF
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': '$DB_NAME',
'USER': '$DB_USERNAME',
'PASSWORD': '$DB_PASSWORD',
'HOST': 'localhost',
'PORT': '5432',
}
}
BROKER_URL = 'redis://127.0.0.1:6379/0'
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'
EOF
echo -e "\n -> Migrating the database or initialize the project \n"
mayan-edms.py initialsetup
echo -e "\n -> Disabling the default NGINX site \n"
rm -f /etc/nginx/sites-enabled/default
echo -e "\n -> Creating a uwsgi.ini file \n"
cat > uwsgi.ini << EOF
[uwsgi]
chdir = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan
chmod-socket = 664
chown-socket = www-data:www-data
env = DJANGO_SETTINGS_MODULE=mayan.settings.production
gid = www-data
logto = /var/log/uwsgi/%n.log
pythonpath = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages
master = True
max-requests = 5000
socket = ${INSTALLATION_DIRECTORY}uwsgi.sock
uid = www-data
vacuum = True
wsgi-file = ${INSTALLATION_DIRECTORY}lib/python2.7/site-packages/mayan/wsgi.py
EOF
echo -e "\n -> Creating the directory for the uWSGI log files \n"
mkdir -p /var/log/uwsgi
echo -e "\n -> Creating the NGINX site file for Mayan EDMS, /etc/nginx/sites-available/mayan \n"
cat > /etc/nginx/sites-available/mayan << EOF
server {
listen 80;
server_name localhost;
location / {
include uwsgi_params;
uwsgi_pass unix:${INSTALLATION_DIRECTORY}uwsgi.sock;
client_max_body_size 30M; # Increse if your plan to upload bigger documents
proxy_read_timeout 30s; # Increase if your document uploads take more than 30 seconds
}
location /static {
alias ${INSTALLATION_DIRECTORY}mayan/media/static;
expires 1h;
}
location /favicon.ico {
alias ${INSTALLATION_DIRECTORY}mayan/media/static/appearance/images/favicon.ico;
expires 1h;
}
}
EOF
echo -e "\n -> Enabling the NGINX site for Mayan EDMS \n"
ln -s /etc/nginx/sites-available/mayan /etc/nginx/sites-enabled/
echo -e "\n -> Creating the supervisor file for the uWSGI process, /etc/supervisor/conf.d/mayan-uwsgi.conf \n"
cat > /etc/supervisor/conf.d/mayan-uwsgi.conf << EOF
[program:mayan-uwsgi]
command = ${INSTALLATION_DIRECTORY}bin/uwsgi --ini ${INSTALLATION_DIRECTORY}uwsgi.ini
user = root
autostart = true
autorestart = true
redirect_stderr = true
EOF
echo -e "\n -> Creating the supervisor file for the Celery worker, /etc/supervisor/conf.d/mayan-celery.conf \n"
cat > /etc/supervisor/conf.d/mayan-celery.conf << EOF
[program:mayan-worker]
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production worker -Ofair -l ERROR
directory = ${INSTALLATION_DIRECTORY}
user = www-data
stdout_logfile = /var/log/mayan/worker-stdout.log
stderr_logfile = /var/log/mayan/worker-stderr.log
autostart = true
autorestart = true
startsecs = 10
stopwaitsecs = 10
killasgroup = true
priority = 998
[program:mayan-beat]
command = ${INSTALLATION_DIRECTORY}bin/python ${INSTALLATION_DIRECTORY}bin/mayan-edms.py celery --settings=mayan.settings.production beat -l ERROR
directory = ${INSTALLATION_DIRECTORY}
user = www-data
numprocs = 1
stdout_logfile = /var/log/mayan/beat-stdout.log
stderr_logfile = /var/log/mayan/beat-stderr.log
autostart = true
autorestart = true
startsecs = 10
stopwaitsecs = 1
killasgroup = true
priority = 998
EOF
echo -e "\n -> Collecting the static files \n"
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
echo -e "\n -> Restarting the services \n"
/etc/init.d/nginx restart
/etc/init.d/supervisor restart

View File

@@ -13,11 +13,12 @@ APP_LIST = (
'checkouts', 'common', 'converter', 'dashboards', 'dependencies', 'checkouts', 'common', 'converter', 'dashboards', 'dependencies',
'django_gpg', 'document_comments', 'document_indexing', 'django_gpg', 'document_comments', 'document_indexing',
'document_parsing', 'document_signatures', 'document_states', 'document_parsing', 'document_signatures', 'document_states',
'documents', 'dynamic_search', 'events', 'file_metadata', 'linking', 'documents', 'dynamic_search', 'events', 'file_caching',
'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring', 'file_metadata', 'linking', 'lock_manager', 'mailer',
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api', 'mayan_statistics', 'metadata', 'mirroring', 'motd', 'navigation',
'smart_settings', 'sources', 'storage', 'tags', 'task_manager', 'ocr', 'permissions', 'platform', 'rest_api', 'smart_settings',
'user_management' 'sources', 'storage', 'tags', 'task_manager', 'user_management',
'weblinks'
) )
LANGUAGE_LIST = ( LANGUAGE_LIST = (

View File

@@ -1,35 +0,0 @@
#!/bin/bash
NAME="mayan-edms"
DJANGODIR=/usr/share/mayan-edms
SOCKFILE=/var/tmp/filesystem.sock
USER=www-data
GROUP=www-data
NUM_WORKERS=3
DJANGO_SETTINGS_MODULE=mayan.settings.production
DJANGO_WSGI_MODULE=mayan.wsgi
TIMEOUT=600
echo "Starting $NAME as `whoami`"
# Activate the virtual environment
cd $DJANGODIR
source bin/activate
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR
# Start your Django Unicorn
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
exec bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--log-level=debug \
--bind=unix:$SOCKFILE \
--timeout=$TIMEOUT

View File

@@ -4,7 +4,7 @@
# BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS # BASE_IMAGE - Bare bones image with the base packages needed to run Mayan EDMS
#### ####
FROM debian:9.8-slim as BASE_IMAGE FROM debian:10.0-slim as BASE_IMAGE
LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com" LABEL maintainer="Roberto Rosario roberto.rosario@mayan-edms.com"
@@ -22,6 +22,7 @@ RUN set -x \
&& DEBIAN_FRONTEND=noninteractive \ && DEBIAN_FRONTEND=noninteractive \
apt-get update \ apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ca-certificates \
exiftool \ exiftool \
ghostscript \ ghostscript \
gpgv \ gpgv \
@@ -29,11 +30,11 @@ apt-get update \
graphviz \ graphviz \
libfuse2 \ libfuse2 \
libmagic1 \ libmagic1 \
libmariadbclient18 \ libmariadb3 \
libreoffice \ libreoffice \
libpq5 \ libpq5 \
poppler-utils \ poppler-utils \
redis-server \ python3-distutils \
sane-utils \ sane-utils \
sudo \ sudo \
supervisor \ supervisor \
@@ -52,22 +53,20 @@ apt-get update \
&& if [ "$(uname -m)" = "armv7l" ]; then \ && if [ "$(uname -m)" = "armv7l" ]; then \
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \ ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \
&& ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \ && ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
; fi \ ; fi
# Discard data when Redis runs out of memory
&& echo "maxmemory-policy allkeys-lru" >> /etc/redis/redis.conf \
# Disable saving the Redis database
echo "save \"\"" >> /etc/redis/redis.conf \
# Only provision 1 database
&& echo "databases 1" >> /etc/redis/redis.conf
#### ####
# BUILDER_IMAGE - This image buildS the Python package and is discarded afterwards # BUILDER_IMAGE - This image builds the Python package and is discarded afterwards
# only the build artifact is carried over to the next image.
#### ####
# Reuse image # Reuse image
FROM BASE_IMAGE as BUILDER_IMAGE FROM BASE_IMAGE as BUILDER_IMAGE
# Python libraries caching
ARG PIP_INDEX_URL
ARG PIP_TRUSTED_HOST
WORKDIR /src WORKDIR /src
# Copy the source files needed to build the Python package # Copy the source files needed to build the Python package
@@ -96,39 +95,40 @@ apt-get install -y --no-install-recommends \
libssl-dev \ libssl-dev \
g++ \ g++ \
gcc \ gcc \
python-dev \ python3-dev \
python-virtualenv \ python3-venv \
&& mkdir -p "${PROJECT_INSTALL_DIR}" \ && mkdir -p "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \ && chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan /src && chown -R mayan:mayan /src
USER mayan USER mayan
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \ RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \ && . "${PROJECT_INSTALL_DIR}/bin/activate" \
&& pip install --no-cache-dir --no-use-pep517 \ && pip install --no-cache-dir \
librabbitmq==1.6.1 \ librabbitmq==2.0.0 \
mysql-python==1.2.5 \ mysqlclient==1.4.2.post1 \
psycopg2==2.7.3.2 \ psycopg2==2.8.3 \
redis==2.10.6 \ redis==3.2.1 \
flower==0.9.3 \
# psutil is needed by ARM builds otherwise gevent and gunicorn fail to start # psutil is needed by ARM builds otherwise gevent and gunicorn fail to start
&& UNAME=`uname -m` && if [ "${UNAME#*arm}" != $UNAME ]; then \ && UNAME=`uname -m` && if [ "${UNAME#*arm}" != $UNAME ]; then \
pip install --no-cache-dir --no-use-pep517 \ pip install --no-cache-dir \
psutil==5.6.2 \ psutil==5.6.2 \
; fi \ ; fi \
# Install the Python packages needed to build Mayan EDMS # Install the Python packages needed to build Mayan EDMS
&& pip install --no-cache-dir --no-use-pep517 -r /src/requirements/build.txt \ && pip install --no-cache-dir -r /src/requirements/build.txt \
# Build Mayan EDMS # Build Mayan EDMS
&& python setup.py sdist \ && python3 setup.py sdist \
# Install the built Mayan EDMS package # Install the built Mayan EDMS package
&& pip install --no-cache-dir --no-use-pep517 dist/mayan* \ && pip install --no-cache-dir dist/mayan* \
# Install the static content # Install the static content
&& mayan-edms.py installjavascript \ && mayan-edms.py installdependencies \
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput && MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}" COPY --chown=mayan:mayan requirements/testing-base.txt "${PROJECT_INSTALL_DIR}"
#### ####
# Final image - BASE_IMAGE + Mayan install directory from the builder image # Final image - BASE_IMAGE + BUILDER_IMAGE artifact (Mayan install directory)
#### ####
FROM BASE_IMAGE FROM BASE_IMAGE
@@ -144,7 +144,7 @@ VOLUME ["/var/lib/mayan"]
ENTRYPOINT ["entrypoint.sh"] ENTRYPOINT ["entrypoint.sh"]
EXPOSE 8000 EXPOSE 8000
CMD ["mayan"] CMD ["run_all"]
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \ RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
&& apt-get clean autoclean \ && apt-get clean autoclean \

View File

@@ -1,4 +1,9 @@
APT_PROXY ?= `/sbin/ip route|awk '/docker0/ { print $$9 }'`:3142 HOST_IP = `/sbin/ip route|awk '/docker0/ { print $$9 }'`
APT_PROXY ?= $(HOST_IP):3142
PIP_INDEX_URL ?= http://$(HOST_IP):3141/root/pypi/+simple/
PIP_TRUSTED_HOST ?= $(HOST_IP)
IMAGE_VERSION ?= `cat docker/rootfs/version` IMAGE_VERSION ?= `cat docker/rootfs/version`
CONSOLE_COLUMNS ?= `echo $$(tput cols)` CONSOLE_COLUMNS ?= `echo $$(tput cols)`
CONSOLE_LINES ?= `echo $$(tput lines)` CONSOLE_LINES ?= `echo $$(tput lines)`
@@ -7,7 +12,7 @@ docker-build: ## Build a new image locally.
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile . docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile .
docker-build-with-proxy: ## Build a new image locally using an APT proxy as APT_PROXY. docker-build-with-proxy: ## Build a new image locally using an APT proxy as APT_PROXY.
docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) . docker build -t mayanedms/mayanedms:$(IMAGE_VERSION) -f docker/Dockerfile --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) .
docker-shell: ## Launch a bash instance inside a running container. Pass the container name via DOCKER_CONTAINER. docker-shell: ## Launch a bash instance inside a running container. Pass the container name via DOCKER_CONTAINER.
docker exec -e TERM=$(TERM) -e "COLUMNS=$(CONSOLE_COLUMNS)" -e "LINES=$(CONSOLE_LINES)" -it $(DOCKER_CONTAINER) /bin/bash docker exec -e TERM=$(TERM) -e "COLUMNS=$(CONSOLE_COLUMNS)" -e "LINES=$(CONSOLE_LINES)" -it $(DOCKER_CONTAINER) /bin/bash
@@ -23,3 +28,13 @@ docker-test-cleanup: ## Delete the test container and the test volume.
docker-test-all: ## Build and executed the test suite in a test container. docker-test-all: ## Build and executed the test suite in a test container.
docker-test-all: docker-build-with-proxy docker-test-all: docker-build-with-proxy
docker run --rm run-tests docker run --rm run-tests
docker-compose-build:
docker-compose -f docker/docker-compose.yml -p mayan-edms build
docker-compose-build-with-proxy:
docker-compose -f docker/docker-compose.yml -p mayan-edms build --build-arg APT_PROXY=$(APT_PROXY) --build-arg PIP_INDEX_URL=$(PIP_INDEX_URL) --build-arg PIP_TRUSTED_HOST=$(PIP_TRUSTED_HOST) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg HTTPS_PROXY=$(HTTPS_PROXY)
docker-compose-up:
docker-compose -f docker/docker-compose.yml -p mayan-edms up

View File

@@ -1,72 +0,0 @@
version: '2.1'
volumes:
broker:
driver: local
app:
driver: local
db:
driver: local
results:
driver: local
services:
broker:
container_name: mayan-edms-broker
image: healthcheck/rabbitmq
environment:
RABBITMQ_DEFAULT_USER: mayan
RABBITMQ_DEFAULT_PASS: mayan
RABBITMQ_DEFAULT_VHOST: mayan
volumes:
- broker:/var/lib/rabbitmq
results:
container_name: mayan-edms-results
image: healthcheck/redis
volumes:
- results:/data
#db:
# container_name: mayan-edms-db
# image: healthcheck/mysql
# environment:
# MYSQL_DATABASE: mayan
# MYSQL_PASSWORD: mayan-password
# MYSQL_ROOT_PASSWORD: root-password
# MYSQL_USER: mayan
# volumes:
# - db:/var/lib/mysql
db:
container_name: mayan-edms-db
image: healthcheck/postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
volumes:
- db:/var/lib/postgresql/data
mayan-edms:
container_name: mayan-edms-app
image: mayan-edms/next
build:
context: ./
args:
- APT_PROXY=172.18.0.1:3142
depends_on:
broker:
condition: service_healthy
db:
condition: service_healthy
results:
condition: service_healthy
environment:
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db
MAYAN_DATABASE_NAME: mayan
MAYAN_DATABASE_PASSWORD: mayan-password
MAYAN_DATABASE_USER: mayan
ports:
- "80:80"
volumes:
- app:/var/lib/mayan

View File

@@ -1,58 +1,130 @@
version: '2.1' version: '3.7'
volumes: networks:
broker: mayan-bridge:
driver: local driver: bridge
app:
driver: local
db:
driver: local
results:
driver: local
services: services:
broker: app:
container_name: mayan-edms-broker build:
image: healthcheck/rabbitmq context: ..
environment: dockerfile: ./docker/Dockerfile
RABBITMQ_DEFAULT_USER: mayan
RABBITMQ_DEFAULT_PASS: mayan
RABBITMQ_DEFAULT_VHOST: mayan
volumes:
- broker:/var/lib/rabbitmq
results:
container_name: mayan-edms-results
image: healthcheck/redis
volumes:
- results:/data
db:
container_name: mayan-edms-db
image: healthcheck/postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayan-password
POSTGRES_USER: mayan
volumes:
- db:/var/lib/postgresql/data
mayan-edms:
container_name: mayan-edms-app
image: mayanedms/mayanedms:latest
depends_on: depends_on:
broker: - postgresql
condition: service_healthy - redis
db: # Enable to use RabbitMQ
condition: service_healthy #- rabbitmq
results: environment: &mayan_env
condition: service_healthy # Enable to use RabbitMQ
environment: # MAYAN_CELERY_BROKER_URL: amqp://mayan:mayanrabbitpass@broker:5672/mayan
MAYAN_BROKER_URL: amqp://mayan:mayan@broker:5672/mayan # Disable Redis Broker to use RabbitMQ as Broker
MAYAN_CELERY_RESULT_BACKEND: redis://results:6379/0 MAYAN_CELERY_BROKER_URL: redis://redis:6379/1
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0
MAYAN_DATABASE_HOST: db MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'postgresql'}}"
MAYAN_DATABASE_NAME: mayan image: mayanedms/mayanedms:3.2.6
MAYAN_DATABASE_PASSWORD: mayan-password networks:
MAYAN_DATABASE_USER: mayan - mayan-bridge
ports: ports:
- "80:8000" - "80:8000"
restart: unless-stopped
volumes: volumes:
- app:/var/lib/mayan - /docker-volumes/mayan-edms/media:/var/lib/mayan
postgresql:
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayandbpass
POSTGRES_USER: mayan
image: postgres:9.6
networks:
- mayan-bridge
restart: unless-stopped
volumes:
- /docker-volumes/mayan-edms/postgres:/var/lib/postgresql/data
redis:
command:
- redis-server
- --databases
- "2"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
image: redis:5.0
networks:
- mayan-bridge
restart: unless-stopped
# Optional services
# celery_flower:
# command:
# - run_celery
# - flower
# depends_on:
# - postgresql
# - redis
# # Enable to use RabbitMQ
# # - rabbitmq
# environment:
# <<: *mayan_env
# image: mayanedms/mayanedms:3.2.6
# networks:
# - mayan-bridge
# ports:
# - "5555:5555"
# restart: unless-stopped
# Enable to use RabbitMQ
# rabbitmq:
# container_name: mayan-edms-rabbitmq
# image: healthcheck/rabbitmq
# environment:
# RABBITMQ_DEFAULT_USER: mayan
# RABBITMQ_DEFAULT_PASS: mayanrabbitpass
# RABBITMQ_DEFAULT_VHOST: mayan
# networks:
# - mayan-bridge
# restart: unless-stopped
# volumes:
# - /docker-volumes/mayan-edms/rabbitmq:/var/lib/rabbitmq
# Enable to run stand alone workers
# worker_fast:
# command:
# - run_worker
# - fast
# depends_on:
# - postgresql
# - redis
# # Enable to use RabbitMQ
# # - rabbitmq
# environment:
# <<: *mayan_env
# image: mayanedms/mayanedms:3.2.6
# networks:
# - mayan-bridge
# restart: unless-stopped
# volumes:
# - /docker-volumes/mayan-edms/media:/var/lib/mayan
# Enable to run stand frontend gunicorn
# frontend:
# command:
# - run_frontend
# depends_on:
# - postgresql
# - redis
# # Enable to use RabbitMQ
# # - rabbitmq
# environment:
# <<: *mayan_env
# image: mayanedms/mayanedms:3.2.6
# networks:
# - mayan-bridge
# ports:
# - "81:8000"
# restart: unless-stopped
# volumes:
# - /docker-volumes/mayan-edms/media:/var/lib/mayan

View File

@@ -1,4 +1,7 @@
#!/bin/sh #!/bin/bash
# Use bash and not sh to support argument slicing "${@:2}"
# sh defaults to dash instead of bash.
set -e set -e
echo "mayan: starting entrypoint.sh" echo "mayan: starting entrypoint.sh"
@@ -6,19 +9,15 @@ INSTALL_FLAG=/var/lib/mayan/system/SECRET_KEY
CONCURRENCY_ARGUMENT=--concurrency= CONCURRENCY_ARGUMENT=--concurrency=
DEFAULT_USER_UID=1000 DEFAULT_USER_UID=1000
DEFAULT_USER_GUID=1000 DEFAULT_USER_GID=1000
export MAYAN_DEFAULT_BROKER_URL=redis://127.0.0.1:6379/0
export MAYAN_DEFAULT_CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/0
export MAYAN_ALLOWED_HOSTS='["*"]' export MAYAN_ALLOWED_HOSTS='["*"]'
export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py export MAYAN_BIN=/opt/mayan-edms/bin/mayan-edms.py
export MAYAN_BROKER_URL=${MAYAN_BROKER_URL:-${MAYAN_DEFAULT_BROKER_URL}}
export MAYAN_CELERY_RESULT_BACKEND=${MAYAN_CELERY_RESULT_BACKEND:-${MAYAN_DEFAULT_CELERY_RESULT_BACKEND}}
export MAYAN_INSTALL_DIR=/opt/mayan-edms export MAYAN_INSTALL_DIR=/opt/mayan-edms
export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/ export MAYAN_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
export MAYAN_MEDIA_ROOT=/var/lib/mayan export MAYAN_MEDIA_ROOT=/var/lib/mayan
export MAYAN_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE:-mayan.settings.production} export MAYAN_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE:-mayan.settings.production}
export DJANGO_SETTINGS_MODULE=${MAYAN_SETTINGS_MODULE}
export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn export MAYAN_GUNICORN_BIN=${MAYAN_PYTHON_BIN_DIR}gunicorn
export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2} export MAYAN_GUNICORN_WORKERS=${MAYAN_GUNICORN_WORKERS:-2}
@@ -26,13 +25,9 @@ export MAYAN_GUNICORN_TIMEOUT=${MAYAN_GUNICORN_TIMEOUT:-120}
export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip
export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-1} MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-0}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1} MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-0}
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-1} MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-0}
echo "mayan: changing uid/guid"
usermod mayan -u ${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
groupmod mayan -g ${MAYAN_USER_GUID:-${DEFAULT_USER_GUID}}
if [ "$MAYAN_WORKER_FAST_CONCURRENCY" -eq 0 ]; then if [ "$MAYAN_WORKER_FAST_CONCURRENCY" -eq 0 ]; then
MAYAN_WORKER_FAST_CONCURRENCY= MAYAN_WORKER_FAST_CONCURRENCY=
@@ -55,11 +50,9 @@ else
fi fi
export MAYAN_WORKER_SLOW_CONCURRENCY export MAYAN_WORKER_SLOW_CONCURRENCY
export CELERY_ALWAYS_EAGER=False # Allow importing of user setting modules
export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT
chown mayan:mayan /var/lib/mayan -R
apt_get_install() { apt_get_install() {
apt-get -q update apt-get -q update
apt-get install -y --force-yes --no-install-recommends --auto-remove "$@" apt-get install -y --force-yes --no-install-recommends --auto-remove "$@"
@@ -67,9 +60,9 @@ apt_get_install() {
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
} }
initialize() { initialsetup() {
echo "mayan: initialize()" echo "mayan: initialsetup()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript" su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies"
} }
os_package_installs() { os_package_installs() {
@@ -86,43 +79,71 @@ pip_installs() {
fi fi
} }
start() { run_all() {
echo "mayan: start()" echo "mayan: start()"
rm -rf /var/run/supervisor.sock rm -rf /var/run/supervisor.sock
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
} }
upgrade() { performupgrade() {
echo "mayan: upgrade()" echo "mayan: performupgrade()"
su mayan -c "${MAYAN_BIN} performupgrade --no-javascript" su mayan -c "${MAYAN_BIN} performupgrade --no-dependencies"
}
make_ready() {
# Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialsetup
else
performupgrade
fi
}
set_uid_guid() {
echo "mayan: changing uid/guid"
usermod mayan -u ${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
groupmod mayan -g ${MAYAN_USER_GID:-${DEFAULT_USER_GID}}
} }
os_package_installs || true os_package_installs || true
pip_installs || true pip_installs || true
chown mayan:mayan /var/lib/mayan -R
case "$1" in case "$1" in
mayan) # Check if this is a new install, otherwise try to upgrade the existing run_initialsetup)
# installation on subsequent starts initialsetup
if [ ! -f $INSTALL_FLAG ]; then ;;
initialize
else
upgrade
fi
start
;;
run-tests) # Check if this is a new install, otherwise try to upgrade the existing run_performupgrade)
# installation on subsequent starts performupgrade
if [ ! -f $INSTALL_FLAG ]; then ;;
initialize
else
upgrade
fi
run-tests.sh
;;
*) su mayan -c "$@"; run_all)
;; make_ready
run_all
;;
run_celery)
run_celery.sh "${@:2}"
;;
run_frontend)
run_frontend.sh
;;
run_tests)
make_ready
run_tests.sh
;;
run_worker)
run_worker.sh "${@:2}"
;;
*)
su mayan -c "$@"
;;
esac esac

View File

@@ -0,0 +1,5 @@
#!/bin/bash
# Use -A and not --app. Both are the same but behave differently
# -A can be located before the command while --app cannot.
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan $@"

View File

@@ -0,0 +1,7 @@
#!/bin/bash
MAYAN_GUNICORN_MAX_REQUESTS=${MAYAN_GUNICORN_MAX_REQUESTS:-500}
MAYAN_GUNICORN_MAX_REQUESTS_JITTERS=${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS:-50}
MAYAN_GUNICORN_WORKER_CLASS=${MAYAN_GUNICORN_WORKER_CLASS:-sync}
su mayan -c "${MAYAN_PYTHON_BIN_DIR}gunicorn -w ${MAYAN_GUNICORN_WORKERS} mayan.wsgi --max-requests ${MAYAN_GUNICORN_MAX_REQUESTS} --max-requests-jitter ${MAYAN_GUNICORN_MAX_REQUESTS_JITTERS} --worker-class ${MAYAN_GUNICORN_WORKER_CLASS} --bind 0.0.0.0:8000 --timeout ${MAYAN_GUNICORN_TIMEOUT}"

View File

@@ -0,0 +1,8 @@
#!/bin/bash
QUEUE_LIST=`MAYAN_WORKER_NAME=$1 su mayan -c "${MAYAN_PYTHON_BIN_DIR}mayan-edms.py platformtemplate worker_queues"`
# Use -A and not --app. Both are the same but behave differently
# -A can be located before the command while --app cannot.
# Pass ${@:2} to allow overriding the defaults arguments
su mayan -c "${MAYAN_PYTHON_BIN_DIR}celery -A mayan worker -Ofair -l ERROR -Q $QUEUE_LIST ${@:2}"

View File

@@ -9,24 +9,32 @@ volumes:
services: services:
db: db:
image: postgres
environment: environment:
POSTGRES_DB: mayan POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayan-password POSTGRES_PASSWORD: mayandbpass
POSTGRES_USER: mayan POSTGRES_USER: mayan
image: postgres
volumes: volumes:
- db:/var/lib/postgresql/data - db:/var/lib/postgresql/data
app: app:
environment:
MAYAN_CELERY_BROKER_URL: redis://redis:6379/1
MAYAN_CELERY_RESULT_BACKEND: redis://redis:6379/0
MAYAN_DATABASES: "{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayandbpass','USER':'mayan','HOST':'db'}}"
image: mayanedms/mayanedms:latest image: mayanedms/mayanedms:latest
ports: ports:
- 80:8000 - 80:8000
environment:
MAYAN_DATABASE_ENGINE: django.db.backends.postgresql
MAYAN_DATABASE_HOST: db
MAYAN_DATABASE_NAME: mayan
MAYAN_DATABASE_PASSWORD: mayan-password
MAYAN_DATABASE_USER: mayan
MAYAN_DATABASE_CONN_MAX_AGE: 0
volumes: volumes:
- app:/var/lib/mayan - app:/var/lib/mayan
redis:
command:
- redis-server
- --databases
- "2"
- --maxmemory-policy
- allkeys-lru
- --save
- ""
image: redis:5.0

View File

@@ -127,9 +127,8 @@ For another setup that offers more performance and scalability refer to the
:: ::
sudo -u mayan MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \ sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \ MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py initialsetup /opt/mayan-edms/bin/mayan-edms.py initialsetup
@@ -148,9 +147,8 @@ For another setup that offers more performance and scalability refer to the
------------------------------------------------------------------------ ------------------------------------------------------------------------
:: ::
sudo MAYAN_DATABASE_ENGINE=django.db.backends.postgresql MAYAN_DATABASE_NAME=mayan \ sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \ MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf /opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
@@ -222,11 +220,11 @@ of a restart or power failure. The Gunicorn workers are increased to 3.
--------------------------------------------------------------------- ---------------------------------------------------------------------
Replace (paying attention to the comma at the end):: Replace (paying attention to the comma at the end)::
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0", MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
with:: with::
MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan", MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
increase the number of Gunicorn workers to 3 in the line (``-w 2`` section):: increase the number of Gunicorn workers to 3 in the line (``-w 2`` section)::

View File

@@ -49,12 +49,7 @@ Finally create and run a Mayan EDMS container::
--name mayan-edms \ --name mayan-edms \
--restart=always \ --restart=always \
-p 80:8000 \ -p 80:8000 \
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \ -e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'172.17.0.1'}}" \
-e MAYAN_DATABASE_HOST=172.17.0.1 \
-e MAYAN_DATABASE_NAME=mayan \
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
-e MAYAN_DATABASE_USER=mayan \
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \ -v /docker-volumes/mayan-edms/media:/var/lib/mayan \
mayanedms/mayanedms:<version> mayanedms/mayanedms:<version>
@@ -108,12 +103,7 @@ instead of the IP address of the Docker host (``172.17.0.1``)::
--network=mayan \ --network=mayan \
--restart=always \ --restart=always \
-p 80:8000 \ -p 80:8000 \
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \ -e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'mayan-edms-postgres'}}" \
-e MAYAN_DATABASE_HOST=mayan-edms-postgres \
-e MAYAN_DATABASE_NAME=mayan \
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
-e MAYAN_DATABASE_USER=mayan \
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \ -v /docker-volumes/mayan-edms/media:/var/lib/mayan \
mayanedms/mayanedms:<version> mayanedms/mayanedms:<version>
@@ -137,102 +127,14 @@ To start the container again::
Environment Variables Environment Variables
--------------------- ---------------------
The Mayan EDMS image can be configure via environment variables. The common set of settings can also be modified via environment variables when
using the Docker image. In addition to the common set of settings, some Docker
``MAYAN_DATABASE_ENGINE`` image specific environment variables are available.
Defaults to ``None``. This environment variable configures the database
backend to use. If left unset, SQLite will be used. The database backends
supported by this Docker image are:
- ``'django.db.backends.postgresql'``
- ``'django.db.backends.mysql'``
- ``'django.db.backends.sqlite3'``
When using the SQLite backend, the database file will be saved in the Docker
volume. The SQLite database as used by Mayan EDMS is meant only for development
or testing, never use it in production.
``MAYAN_DATABASE_NAME``
Defaults to 'mayan'. This optional environment variable can be used to define
the database name that Mayan EDMS will connect to. For more information read
the pertinent Django documentation page:
:django-docs:`Connecting to the database <ref/databases/#connecting-to-the-database>`
``MAYAN_DATABASE_USER``
Defaults to 'mayan'. This optional environment variable is used to set the
username that will be used to connect to the database. For more information
read the pertinent Django documentation page:
:django-docs:`Settings, USER <ref/settings/#user>`
``MAYAN_DATABASE_PASSWORD``
Defaults to ''. This optional environment variable is used to set the
password that will be used to connect to the database. For more information
read the pertinent Django documentation page:
:django-docs:`Settings, PASSWORD <ref/settings/#password>`
``MAYAN_DATABASE_HOST``
Defaults to `None`. This optional environment variable is used to set the
hostname that will be used to connect to the database. This can be the
hostname of another container or an IP address. For more information read
the pertinent Django documentation page:
:django-docs:`Settings, HOST <ref/settings/#host>`
``MAYAN_DATABASE_PORT``
Defaults to `None`. This optional environment variable is used to set the
port number to use when connecting to the database. An empty string means
the default port. Not used with SQLite. For more information read the
pertinent Django documentation page:
:django-docs:`Settings, PORT <ref/settings/#port>`
``MAYAN_BROKER_URL``
This optional environment variable determines the broker that Celery will use
to relay task messages between the frontend code and the background workers.
For more information read the pertinent Celery Kombu documentation page: `Broker URL`_
.. _Broker URL: http://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls
This Docker image supports using Redis and RabbitMQ as brokers.
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
variables are specified, the built-in Redis server inside the container will
be disabled.
``MAYAN_CELERY_RESULT_BACKEND``
This optional environment variable determines the results backend that Celery
will use to relay result messages from the background workers to the frontend
code. For more information read the pertinent Celery Kombu documentation page:
`Task result backend settings`_
.. _Task result backend settings: http://docs.celeryproject.org/en/3.1/configuration.html#celery-result-backend
This Docker image supports using Redis and RabbitMQ as result backends.
Caveat: If the `MAYAN_BROKER_URL` and `MAYAN_CELERY_RESULT_BACKEND` environment
variables are specified, the built-in Redis server inside the container will
be disabled.
``MAYAN_SETTINGS_MODULE`` ``MAYAN_SETTINGS_MODULE``
Optional. Allows loading an alternate settings file. Optional. Allows loading an alternate settings file.
``MAYAN_DATABASE_CONN_MAX_AGE``
Amount in seconds to keep a database connection alive. Allow reuse of database
connections. For more information read the pertinent Django documentation
page: :django-docs:`Settings, CONN_MAX_AGE <ref/settings/#conn-max-age>`
According to new information Gunicorn's microthreads don't share connections
and will exhaust the available Postgres connections available if a number
other than 0 is used. Reference: https://serverfault.com/questions/635100/django-conn-max-age-persists-connections-but-doesnt-reuse-them-with-postgresq
and https://github.com/benoitc/gunicorn/issues/996
``MAYAN_GUNICORN_WORKERS`` ``MAYAN_GUNICORN_WORKERS``
Optional. This environment variable controls the number of frontend workers Optional. This environment variable controls the number of frontend workers
@@ -269,12 +171,21 @@ number of CPUs detected).
Optional. Changes the UID of the ``mayan`` user internal to the Docker Optional. Changes the UID of the ``mayan`` user internal to the Docker
container. Defaults to 1000. container. Defaults to 1000.
``MAYAN_USER_GUID`` ``MAYAN_USER_GID``
Optional. Changes the GUID of the ``mayan`` user internal to the Docker Optional. Changes the GID of the ``mayan`` user internal to the Docker
container. Defaults to 1000. container. Defaults to 1000.
Included drivers
----------------
The Docker image supports using Redis and RabbitMQ as result backends. For
databases, the image includes support for PostgreSQL and MySQL/MariaDB.
Support for additional brokers or databases may be added using the
``MAYAN_APT_INSTALL`` environment variable.
.. _docker-accessing-outside-data: .. _docker-accessing-outside-data:
Accessing outside data Accessing outside data
@@ -442,6 +353,7 @@ These are:
Nightly images Nightly images
============== ==============
The continuous integration pipeline used for testing development builds also The continuous integration pipeline used for testing development builds also
produces a resulting Docker image. These are build automatically and their produces a resulting Docker image. These are build automatically and their
stability is not guaranteed. They should never be used in production. stability is not guaranteed. They should never be used in production.

View File

@@ -94,11 +94,11 @@ For the Docker image, launch a separate RabbitMQ container
docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3 docker run -d --name mayan-edms-rabbitmq -e RABBITMQ_DEFAULT_USER=mayan -e RABBITMQ_DEFAULT_PASS=mayanrabbitmqpassword -e RABBITMQ_DEFAULT_VHOST=mayan rabbitmq:3
Pass the MAYAN_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls) Pass the MAYAN_CELERY_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
to the Mayan EDMS container so that it uses the RabbitMQ container the to the Mayan EDMS container so that it uses the RabbitMQ container the
message broker:: message broker::
-e MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan", -e MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
When tasks finish, they leave behind a return status or the result of a When tasks finish, they leave behind a return status or the result of a
calculation, these are stored for a while so that whoever requested the calculation, these are stored for a while so that whoever requested the

View File

@@ -15,7 +15,8 @@ The current document sources supported are:
- IMAP email - Same as the ``POP3`` email source but for email accounts using - IMAP email - Same as the ``POP3`` email source but for email accounts using
the ``IMAP`` protocol. the ``IMAP`` protocol.
- Watch folder - A filesystem folder that is scanned periodically for files. - Watch folder - A filesystem folder that is scanned periodically for files.
Any file in the watch folder is automatically uploaded. Any file in the watch folder is automatically uploaded. When the upload for a
file is completed, the file is removed from source folder.
- Staging folder - Folder where networked attached scanned can save image - Staging folder - Folder where networked attached scanned can save image
files. The files in these staging folders are scanned and a preview is files. The files in these staging folders are scanned and a preview is
generated to help the process of upload. Staging folders and Watch folders generated to help the process of upload. Staging folders and Watch folders

View File

@@ -19,7 +19,6 @@ Changes
GitLab issue #625. Thanks to Jesaja Everling (@jeverling) GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research. for the report and the research.
Removals Removals
-------- --------

207
docs/releases/3.3.rst Normal file
View File

@@ -0,0 +1,207 @@
Version 3.3
===========
Released: XX XX, 2019
Changes
-------
- Add support for icon shadows.
- Add icons and no-result template to the object error log view and
links.
- Use Select2 widget for the document type selection form.
- Backport the vertical main menu update. This update splits the previous
main menu into a new menu in the same location as the previous one
now called the top bar, and a new vertical main menu on the left side.
The vertical menu remain open even when clicking on items and upon
a browser refresh will also restore its state to match the selected
view.
- Backport workflow preview refactor. GitLab issue #532.
- Add support for source column inheritance.
- Add support for source column exclusion.
- Backport workflow context support.
- Backport workflow transitions field support.
- Backport workflow email action.
- Backport individual index rebuild support.
- Rename the installjavascript command to installdependencies.
- Remove database conversion command.
- Remove support for quoted configuration entries. Support unquoted,
nested dictionaries in the configuration. Requires manual
update of existing config.yml files.
- Support user specified locations for the configuration file with the
CONFIGURATION_FILEPATH (MAYAN_CONFIGURATION_FILEPATH environment variable), and
CONFIGURATION_LAST_GOOD_FILEPATH
(MAYAN_CONFIGURATION_LAST_GOOD_FILEPATH environment variable) settings.
- Move bootstrapped settings code to their own module in the smart_settings apps.
- Remove individual database configuration options. All database configuration
is now done using MAYAN_DATABASES to mirror Django way of doing database setup.
- Added support for YAML encoded environment variables to the platform
templates apps.
- Move YAML code to its own module. Code now resides in common.serialization
in the form of two new functions: yaml_load and yaml_dump.
- Move Django and Celery settings. Django settings now reside in the smart
settings app. Celery settings now reside in the task manager app.
- Backport FakeStorageSubclass from versions/next. Placeholder class to allow
serializing the real storage subclass to support migrations.
Used by all configurable storages.
- Support checking in and out multiple documents.
- Remove encapsulate helper.
- Add support for menu inheritance.
- Emphasize source column labels.
- Backport file cache manager app.
- Convert document image cache to use file cache manager app.
Add setting DOCUMENTS_CACHE_MAXIMUM_SIZE defaults to 500 MB.
- Update Celery to version 4.3.0. Settings changed:
MAYAN_BROKER_URL to MAYAN_CELERY_BROKER_URL,
MAYAN_CELERY_ALWAYS_EAGER to MAYAN_CELERY_TASK_ALWAYS_EAGER.
- Replace djcelery and replace it with django-celery-beat.
- Update Celery to version 4.3.0 with 55e9b2263cbdb9b449361412fd18d8ee0a442dd3
from versions/next, code from GitLab issue #594 and GitLab merge request !55.
Thanks to Jakob Haufe (@sur5r) and Jesaja Everling (@jeverling)
for much of the research and code updates.
- Support wildcard MIME type associations for the file metadata drivers.
- Rename MAYAN_GUID to MAYAN_GID
- Update Gunicorn to use sync workers.
- Include devpi-server as a development dependency.
- Update default Docker stack file.
- Remove Redis from the Docker image.
- Add Celery flower to the Docker image.
- Allow PIP proxying to the Docker image during build.
- Default Celery worker concurrency to 0 (auto).
- Set DJANGO_SETTINGS_MODULE environment variable to make it
available to sub processes.
- Add entrypoint commands to run single workers, single gunicorn
or single celery commands like "flower".
- Add platform template to return queues for a worker.
- Remove task inspection from task manager app.
- Move pagination navigation inside the toolbar.
- Remove document image clear link and view.
This is now handled by the file caching app.
- Add web links app.
- Add support to display column help text
as a tooltip.
- Update numeric dashboard widget to display
thousand commas.
- Add support for disabling document pages.
Removals
--------
- Database conversion. Reason for removal. The database conversions support
provided by this feature (SQLite to PostgreSQL) was being confused with
database migrations and upgrades.
Database upgrades are the responsibility of the app and the framework.
Database conversions however are not the responsibility of the app (Mayan),
they are the responsibility of the framework.
Database conversion is outside the scope of what Mayan does but we added
the code, management command, instructions and testing setup to provide
this to our users until the framework (Django) decided to add this
themselves (like they did with migrations).
Continued confusion about the purpose of the feature and confusion about
how errors with this feature were a reflexion of the code quality of
Mayannecessitated the removal of the database conversion feature.
- Django environ
Upgrading from a previous version
---------------------------------
If installed via Python's PIP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Remove deprecated requirements::
sudo -u mayan curl https://gitlab.com/mayan-edms/mayan-edms/raw/master/removals.txt -o /tmp/removals.txt && sudo -u mayan /opt/mayan-edms/bin/pip uninstall -y -r /tmp/removals.txt
Type in the console::
/opt/mayan-edms/bin/pip install mayan-edms==3.3
the requirements will also be updated automatically.
Using Git
^^^^^^^^^
If you installed Mayan EDMS by cloning the Git repository issue the commands::
git reset --hard HEAD
git pull
otherwise download the compressed archived and uncompress it overriding the
existing installation.
Remove deprecated requirements::
pip uninstall -y -r removals.txt
Next upgrade/add the new requirements::
pip install --upgrade -r requirements.txt
Common steps
^^^^^^^^^^^^
Perform these steps after updating the code from either step above.
Make a backup of your supervisord file::
sudo cp /etc/supervisor/conf.d/mayan.conf /etc/supervisor/conf.d/mayan.conf.bck
Update the supervisord configuration file. Replace the environment
variables values show here with your respective settings. This step will refresh
the supervisord configuration file with the new queues and the latest
recommended layout::
sudo MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py platformtemplate supervisord > /etc/supervisor/conf.d/mayan.conf
Edit the supervisord configuration file and update any setting the template
generator missed::
sudo vi /etc/supervisor/conf.d/mayan.conf
Migrate existing database schema with::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py performupgrade
Add new static media::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media /opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
The upgrade procedure is now complete.
Backward incompatible changes
-----------------------------
- Update quoted settings to be unquoted:
- COMMON_SHARED_STORAGE_ARGUMENTS
- CONVERTER_GRAPHICS_BACKEND_ARGUMENTS
- DOCUMENTS_CACHE_STORAGE_BACKEND_ARGUMENTS
- DOCUMENTS_STORAGE_BACKEND_ARGUMENTS
- FILE_METADATA_DRIVERS_ARGUMENTS
- SIGNATURES_STORAGE_BACKEND_ARGUMENTS
Bugs fixed or issues closed
---------------------------
- :gitlab-issue:`526` RuntimeWarning: Never call result.get() within a task!
- :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified
- :gitlab-issue:`540` hint-outdated/update documentation
- :gitlab-issue:`594` 3.2b1: Unable to install/run under Python 3.5/3.6/3.7
- :gitlab-issue:`634` Failing docker entrypoint when using secret config
- :gitlab-issue:`635` Build a docker image for Python3
- :gitlab-issue:`644` Update sane-utils package in docker image.
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -20,6 +20,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
3.3
3.2.6 3.2.6
3.2.5 3.2.5
3.2.4 3.2.4

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
__title__ = 'Mayan EDMS' __title__ = 'Mayan EDMS'
__version__ = '3.2.6' __version__ = '3.2.6'
__build__ = 0x030206 __build__ = 0x030206
__build_string__ = 'v3.2.6_Wed Jul 10 03:18:15 2019 -0400' __build_string__ = 'v3.2.6-68-gab601f9180_Wed Jul 17 04:30:11 2019 -0400'
__django_version__ = '1.11' __django_version__ = '1.11'
__author__ = 'Roberto Rosario' __author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com' __author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -12,6 +12,7 @@ logger = logging.getLogger(__name__)
class ModelPermission(object): class ModelPermission(object):
_functions = {} _functions = {}
_inheritances = {} _inheritances = {}
_manager_names = {}
_registry = {} _registry = {}
@classmethod @classmethod
@@ -97,6 +98,24 @@ class ModelPermission(object):
def get_inheritance(cls, model): def get_inheritance(cls, model):
return cls._inheritances[model] return cls._inheritances[model]
@classmethod
def get_manager(cls, model):
try:
manager_name = cls.get_manager_name(model=model)
except KeyError:
manager_name = None
if manager_name:
manager = getattr(model, manager_name)
else:
manager = model._meta.default_manager
return manager
@classmethod
def get_manager_name(cls, model):
return cls._manager_names[model]
@classmethod @classmethod
def register_function(cls, model, function): def register_function(cls, model, function):
cls._functions[model] = function cls._functions[model] = function
@@ -104,3 +123,7 @@ class ModelPermission(object):
@classmethod @classmethod
def register_inheritance(cls, model, related): def register_inheritance(cls, model, related):
cls._inheritances[model] = related cls._inheritances[model] = related
@classmethod
def register_manager(cls, model, manager_name):
cls._manager_names[model] = manager_name

View File

@@ -200,28 +200,26 @@ class AccessControlListManager(models.Manager):
return result return result
def check_access(self, obj, permissions, user, manager=None): def check_access(self, obj, permissions, user):
# Allow specific managers for models that have more than one # Allow specific managers for models that have more than one
# for example the Document model when checking for access for a trashed # for example the Document model when checking for access for a trashed
# document. # document.
if manager: meta = getattr(obj, '_meta', None)
source_queryset = manager.all()
if not meta:
logger.debug(
ugettext(
'Object "%s" is not a model and cannot be checked for '
'access.'
) % force_text(obj)
)
return True
else: else:
meta = getattr(obj, '_meta', None) manager = ModelPermission.get_manager(model=obj._meta.model)
source_queryset = manager.all()
if not meta: restricted_queryset = manager.none()
logger.debug(
ugettext(
'Object "%s" is not a model and cannot be checked for '
'access.'
) % force_text(obj)
)
return True
else:
source_queryset = obj._meta.default_manager.all()
restricted_queryset = obj._meta.default_manager.none()
for permission in permissions: for permission in permissions:
# Default relationship betweens permissions is OR # Default relationship betweens permissions is OR
# TODO: Add support for AND relationship # TODO: Add support for AND relationship

View File

@@ -4,6 +4,7 @@ from django.template.loader import get_template
class IconDriver(object): class IconDriver(object):
context = {}
_registry = {} _registry = {}
@classmethod @classmethod
@@ -14,6 +15,17 @@ class IconDriver(object):
def register(cls, driver_class): def register(cls, driver_class):
cls._registry[driver_class.name] = driver_class cls._registry[driver_class.name] = driver_class
def get_context(self):
return self.context
def render(self, extra_context=None):
context = self.get_context()
if extra_context:
context.update(extra_context)
return get_template(template_name=self.template_name).render(
context=context
)
class FontAwesomeDriver(IconDriver): class FontAwesomeDriver(IconDriver):
name = 'fontawesome' name = 'fontawesome'
@@ -22,10 +34,8 @@ class FontAwesomeDriver(IconDriver):
def __init__(self, symbol): def __init__(self, symbol):
self.symbol = symbol self.symbol = symbol
def render(self): def get_context(self):
return get_template(template_name=self.template_name).render( return {'symbol': self.symbol}
context={'symbol': self.symbol}
)
class FontAwesomeDualDriver(IconDriver): class FontAwesomeDualDriver(IconDriver):
@@ -36,23 +46,21 @@ class FontAwesomeDualDriver(IconDriver):
self.primary_symbol = primary_symbol self.primary_symbol = primary_symbol
self.secondary_symbol = secondary_symbol self.secondary_symbol = secondary_symbol
def render(self): def get_context(self):
return get_template(template_name=self.template_name).render( return {
context={ 'data': (
'data': ( {
{ 'class': 'fas fa-circle',
'class': 'fas fa-circle', 'transform': 'down-3 right-10',
'transform': 'down-3 right-10', 'mask': 'fas fa-{}'.format(self.primary_symbol)
'mask': 'fas fa-{}'.format(self.primary_symbol) },
}, {'class': 'far fa-circle', 'transform': 'down-3 right-10'},
{'class': 'far fa-circle', 'transform': 'down-3 right-10'}, {
{ 'class': 'fas fa-{}'.format(self.secondary_symbol),
'class': 'fas fa-{}'.format(self.secondary_symbol), 'transform': 'shrink-4 down-3 right-10'
'transform': 'shrink-4 down-3 right-10' },
}, )
) }
}
)
class FontAwesomeCSSDriver(IconDriver): class FontAwesomeCSSDriver(IconDriver):
@@ -62,10 +70,8 @@ class FontAwesomeCSSDriver(IconDriver):
def __init__(self, css_classes): def __init__(self, css_classes):
self.css_classes = css_classes self.css_classes = css_classes
def render(self): def get_context(self):
return get_template(template_name=self.template_name).render( return {'css_classes': self.css_classes}
context={'css_classes': self.css_classes}
)
class FontAwesomeMasksDriver(IconDriver): class FontAwesomeMasksDriver(IconDriver):
@@ -75,23 +81,23 @@ class FontAwesomeMasksDriver(IconDriver):
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data
def render(self): def get_context(self):
return get_template(template_name=self.template_name).render( return {'data': self.data}
context={'data': self.data}
)
class FontAwesomeLayersDriver(IconDriver): class FontAwesomeLayersDriver(IconDriver):
name = 'fontawesome-layers' name = 'fontawesome-layers'
template_name = 'appearance/icons/font_awesome_layers.html' template_name = 'appearance/icons/font_awesome_layers.html'
def __init__(self, data): def __init__(self, data, shadow_class=None):
self.data = data self.data = data
self.shadow_class = shadow_class
def render(self): def get_context(self):
return get_template(template_name=self.template_name).render( return {
context={'data': self.data} 'data': self.data,
) 'shadow_class': self.shadow_class,
}
class Icon(object): class Icon(object):

View File

@@ -12,7 +12,7 @@
} }
body { body {
padding-top: 70px; padding-top: 60px;
} }
.navbar-brand { .navbar-brand {
@@ -70,7 +70,8 @@ img.lazy-load-carousel {
} }
.label-tag { .label-tag {
text-shadow: 0px 0px 2px #000 text-shadow: 0px 0px 2px #000;
box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.5);
} }
.fancybox-nav span { .fancybox-nav span {
@@ -88,19 +89,17 @@ hr {
} }
.btn-block { .btn-block {
border-top: 2px solid rgba(255, 255, 255, 0.7);
border-left: 2px solid rgba(255, 255, 255, 0.7);
border-right: 2px solid rgba(0, 0, 0, 0.7);
border-bottom: 2px solid rgba(0, 0, 0, 0.7);
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
margin-bottom: 15px; margin-bottom: 15px;
white-space: normal;
min-height: 120px; min-height: 120px;
padding-top: 20px;
padding-bottom: 1px; padding-bottom: 1px;
} padding-top: 20px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 1);
.btn-block .fa { white-space: normal;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
.btn-block {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
} }
.radio ul li { .radio ul li {
@@ -112,14 +111,10 @@ a i {
} }
.dashboard-widget { .dashboard-widget {
box-shadow: 1px 1px 1px rgba(0,0,0,0.3); box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
border: 1px solid black; border: 1px solid black;
} }
.dashboard-widget .panel-heading i {
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
}
.dashboard-widget-icon { .dashboard-widget-icon {
font-size: 200%; font-size: 200%;
} }
@@ -170,7 +165,7 @@ a i {
} }
.navbar-collapse { .navbar-collapse {
border-top: 1px solid transparent; border-top: 1px solid transparent;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
} }
.navbar-fixed-top { .navbar-fixed-top {
top: 0; top: 0;
@@ -213,6 +208,22 @@ a i {
font-weight: bold; font-weight: bold;
} }
.source-column-label {
font-weight: bold;
}
.panel-highlighted {
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000;
}
.panel-highlighted:hover {
box-shadow: 0px 0px 3px #18bc9c, 10px 10px 20px #000000, 0px 0px 8px #000000;
}
.panel-item:not(.panel-highlighted):hover {
box-shadow: 0px 0px 8px #000000;
}
/* Content */ /* Content */
@media (min-width:1200px) { @media (min-width:1200px) {
.container-fluid { .container-fluid {
@@ -242,14 +253,6 @@ a i {
margin: auto; margin: auto;
} }
.thin_border {
border: 1px solid black;
display: block;
margin-left: auto;
margin-right: auto;
}
.thin_border-thumbnail { .thin_border-thumbnail {
display: block; display: block;
max-width: 100%; max-width: 100%;
@@ -259,10 +262,18 @@ a i {
margin: auto; margin: auto;
} }
/* Must go after .thin_border-thumbnail */
.thin_border {
border: 1px solid black;
display: inline;
margin-left: 0px;
margin-right: 0px;
}
#ajax-spinner { #ajax-spinner {
position: fixed; position: fixed;
top: 12px; top: 16px;
right: 10px; left: 10px;
z-index: 9999; z-index: 9999;
width: 25px; width: 25px;
height: 25px; height: 25px;
@@ -328,7 +339,7 @@ a i {
.main { .main {
padding-right: 0px; padding-right: 0px;
padding-left: 0px; padding-left: 0px;
/*margin-left: 210px;*/ margin-left: 210px;
} }
} }
@@ -410,3 +421,139 @@ a i {
.btn-list { .btn-list {
margin-bottom: 2px; margin-bottom: 2px;
} }
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/* menu_main */
/* Hide for mobile, show later */
#menu-main {
display: none;
background-color: #2c3e50;
border-right: 1px solid #18bc9c;
bottom: 0;
left: 0;
overflow-x: hidden;
overflow-y: auto;
padding-top: 10px;
position: fixed;
top: 51px;
width: 210px;
z-index: 1000;
}
@media (min-width: 768px) {
#menu-main {
display: block;
}
.navbar-brand {
text-align: center;
width: 210px;
}
}
.main .page-header {
margin-top: 0;
}
.navbar-brand {
}
.navbar-brand {
outline: none;
}
.container-fluid {
margin-right: 0px;
margin-left: 0px;
width: 100%;
}
#accordion-sidebar a {
padding: 10px 15px;
}
#accordion-sidebar a[aria-expanded="true"] {
background: #1a242f;
}
#accordion-sidebar .panel {
border: 0px;
}
#accordion-sidebar a {
text-decoration: none;
outline: none;
position: relative;
display: block;
}
#accordion-sidebar .panel-heading {
background-color: #2c3e50;
color: white;
padding: 0px;
}
#accordion-sidebar .panel-heading:hover {
background-color: #517394;
}
#accordion-sidebar > .panel > div > .panel-body > ul > li > a:hover {
background-color: #517394;
}
#accordion-sidebar > .panel > div > .panel-body > ul > li.active {
background: #1a242f;
}
#accordion-sidebar .panel-title {
font-size: 15px;
}
#accordion-sidebar .panel-body {
font-size: 13px;
border: 0px;
background-color: #2c3e50;
padding-top: 5px;
padding-left: 20px;
padding-right: 0px;
padding-bottom: 0px;
}
#accordion-sidebar .panel-body li {
padding: 0px;
}
#accordion-sidebar .panel-body a {
color: white;
text-decoration: none;
padding: 9px;
}
.navbar-fixed-top {
box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.4);
}
.toolbar {
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 1px 1px 2px rgba(0, 0, 0, .3);
margin-bottom: 10px;
padding-bottom: 8px;
padding-left: 12px;
padding-right: 15px;
padding-top: 8px;
}
#body-plain {
padding-top: 0px;
margin-top: 10px;
}

View File

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

View File

@@ -17,28 +17,45 @@ class MayanApp {
// Class methods and variables // Class methods and variables
static MultiObjectFormProcess ($form, options) { static countChecked() {
/* var checkCount = $('.check-all-slave:checked').length;
* ajaxForm callback to add the external item checkboxes to the
* submitted form
*/
if ($form.hasClass('form-multi-object-action')) { if (checkCount) {
// Turn form data into an object $('#multi-item-title').hide();
var formArray = $form.serializeArray().reduce(function (obj, item) { $('#multi-item-actions').show();
obj[item.name] = item.value; } else {
return obj; $('#multi-item-title').show();
}, {}); $('#multi-item-actions').hide();
}
}
// Add all checked checkboxes to the form data static setupMultiItemActions () {
$('.form-multi-object-action-checkbox:checked').each(function() { $('body').on('change', '.check-all-slave', function () {
var $this = $(this); MayanApp.countChecked();
formArray[$this.attr('name')] = $this.attr('value'); });
$('body').on('click', '.btn-multi-item-action', function (event) {
var id_list = [];
$('.check-all-slave:checked').each(function (index, value) {
//Split the name (ie:"pk_200") and extract only the ID
id_list.push(value.name.split('_')[1]);
});
event.preventDefault();
partialNavigation.setLocation(
$(this).attr('href') + '?id_list=' + id_list.join(',')
);
});
}
static setupNavBarState () {
$('body').on('click', '.a-main-menu-accordion-link', function (event) {
console.log('ad');
$('.a-main-menu-accordion-link').each(function (index, value) {
$(this).parent().removeClass('active');
}); });
// Set the form data as the data to send $(this).parent().addClass('active');
options.data = formArray; });
}
} }
static updateNavbarState () { static updateNavbarState () {
@@ -155,16 +172,18 @@ class MayanApp {
var self = this; var self = this;
this.setupAJAXSpinner(); this.setupAJAXSpinner();
this.setupAutoSubmit();
this.setupFormHotkeys(); this.setupFormHotkeys();
this.setupFullHeightResizing(); this.setupFullHeightResizing();
this.setupItemsSelector(); this.setupItemsSelector();
MayanApp.setupMultiItemActions();
this.setupNavbarCollapse(); this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor(); this.setupNewWindowAnchor();
$.each(this.ajaxMenusOptions, function(index, value) { $.each(this.ajaxMenusOptions, function(index, value) {
value.app = self; value.app = self;
app.doRefreshAJAXMenu(value); app.doRefreshAJAXMenu(value);
}); });
this.setupPanelSelection();
partialNavigation.initialize(); partialNavigation.initialize();
} }
@@ -188,14 +207,6 @@ class MayanApp {
}); });
} }
setupAutoSubmit () {
$('body').on('change', '.select-auto-submit', function () {
if ($(this).val()) {
$(this.form).trigger('submit');
}
});
}
setupFormHotkeys () { setupFormHotkeys () {
$('body').on('keypress', '.form-hotkey-enter', function (e) { $('body').on('keypress', '.form-hotkey-enter', function (e) {
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) { if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
@@ -226,9 +237,22 @@ class MayanApp {
app.lastChecked = null; app.lastChecked = null;
$('body').on('click', '.check-all', function (event) { $('body').on('click', '.check-all', function (event) {
var $this = $(this);
var checked = $(event.target).prop('checked'); var checked = $(event.target).prop('checked');
var $checkBoxes = $('.check-all-slave'); var $checkBoxes = $('.check-all-slave');
if (checked === undefined) {
checked = $this.data('checked');
checked = !checked;
$this.data('checked', checked);
if (checked) {
$this.find('[data-fa-i2svg]').addClass($this.data('icon-checked')).removeClass($this.data('icon-unchecked'));
} else {
$this.find('[data-fa-i2svg]').addClass($this.data('icon-unchecked')).removeClass($this.data('icon-checked'));
}
}
$checkBoxes.prop('checked', checked); $checkBoxes.prop('checked', checked);
$checkBoxes.trigger('change'); $checkBoxes.trigger('change');
}); });
@@ -274,6 +298,58 @@ class MayanApp {
}); });
} }
setupPanelSelection () {
var app = this;
// Setup panel highlighting on check
$('body').on('change', '.check-all-slave', function (event) {
var checked = $(event.target).prop('checked');
if (checked) {
$(this).closest('.panel-item').addClass('panel-highlighted');
} else {
$(this).closest('.panel-item').removeClass('panel-highlighted');
}
});
$('body').on('click', '.panel-item', function (event) {
var $this = $(this);
var targetSrc = $(event.target).prop('src');
var targetHref = $(event.target).prop('href');
var targetIsButton = event.target.tagName === 'BUTTON';
var lastChecked = null;
if ((targetSrc === undefined) && (targetHref === undefined) && (targetIsButton === false)) {
var $checkbox = $this.find('.check-all-slave');
var checked = $checkbox.prop('checked');
if (checked) {
$checkbox.prop('checked', '');
$checkbox.trigger('change');
} else {
$checkbox.prop('checked', 'checked');
$checkbox.trigger('change');
}
if(!app.lastChecked) {
app.lastChecked = $checkbox;
}
if (event.shiftKey) {
var $checkBoxes = $('.check-all-slave');
var start = $checkBoxes.index($checkbox);
var end = $checkBoxes.index(app.lastChecked);
$checkBoxes.slice(
Math.min(start, end), Math.max(start, end) + 1
).prop('checked', app.lastChecked.prop('checked')).trigger('change');
}
app.lastChecked = $checkbox;
window.getSelection().removeAllRanges();
}
});
}
setupScrollView () { setupScrollView () {
$('.scrollable').scrollview(); $('.scrollable').scrollview();
} }

View File

@@ -136,6 +136,9 @@
}, },
{% endfor %} {% endfor %}
]; ];
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script> </script>
{% block javascript %}{% endblock %} {% block javascript %}{% endblock %}

View File

@@ -33,7 +33,7 @@
} }
</script> </script>
</head> </head>
<body> <body id="body-plain">
{% block content_plain %}{% endblock %} {% block content_plain %}{% endblock %}
<script src="{% static 'appearance/node_modules/jquery/dist/jquery.min.js' %}" type="text/javascript"></script> <script src="{% static 'appearance/node_modules/jquery/dist/jquery.min.js' %}" type="text/javascript"></script>

View File

@@ -11,41 +11,9 @@
{% include 'appearance/no_results.html' %} {% include 'appearance/no_results.html' %}
</div> </div>
{% else %} {% else %}
<h4> {% include "appearance/list_header.html" %}
{% if page_obj %} {% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% if page_obj.paginator.num_pages != 1 %}
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
{% else %}
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
</h4>
<hr>
<div class="well center-block"> <div class="well center-block">
<div class="clearfix">
<div class="pull-right">
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
{% if object_list %}
{% if not hide_multi_item_actions %}
{% get_multi_item_links_form object_list %}
{% endif %}
{% if multi_item_actions %}
<fieldset style="margin-top: -10px;">
<input class="check-all" type="checkbox"/>&nbsp;
{{ multi_item_form }}
</fieldset>
{% endif %}
{% endif %}
</form>
</div>
</div>
{% if object_list %}
<hr style="border-bottom: 1px solid lightgrey;">
{% endif %}
<div class="row row-items"> <div class="row row-items">
{% for object in object_list %} {% for object in object_list %}
<div class="{{ column_class|default:'col-xs-12 col-sm-4 col-md-3 col-lg-2' }}"> <div class="{{ column_class|default:'col-xs-12 col-sm-4 col-md-3 col-lg-2' }}">
@@ -53,9 +21,9 @@
<div class="panel-heading"> <div class="panel-heading">
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<label for="id_indexes_0"> <label for="id_indexes_0" style="cursor: auto;">
{% if multi_item_actions %} {% if links_multi_menus_results %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" type="checkbox" name="pk_{{ object.pk }}" /> <input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" style="cursor: pointer;" type="checkbox" />
{% endif %} {% endif %}
<span style="color: white; word-break: break-all; overflow-wrap: break-word;"> <span style="color: white; word-break: break-all; overflow-wrap: break-word;">
@@ -68,12 +36,7 @@
{% else %} {% else %}
{% navigation_get_source_columns source=object only_identifier=True as source_column %} {% navigation_get_source_columns source=object only_identifier=True as source_column %}
{% navigation_source_column_resolve column=source_column as column_value %} {% navigation_source_column_resolve column=source_column as column_value %}
{{ column_value }}
{% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %}
<a href="{% navigation_source_column_get_absolute_url source_column=source_column obj=object %}">{{ column_value }}</a>
{% else %}
{{ column_value }}
{% endif %}
{% endif %} {% endif %}
</span> </span>
</label> </label>
@@ -82,11 +45,10 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% if not hide_columns %} {% if not hide_columns %}
{% navigation_get_source_columns source=object exclude_identifier=True as source_columns %} {% navigation_get_source_columns source=object exclude_identifier=True as source_columns %}
{% for column in source_columns %} {% for column in source_columns %}
<div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}{{ column.label }}: {% endif %}{{ column_value }}{% endif %}</div> <div class="text-center" style="">{% navigation_source_column_resolve column=column as column_value %}{% if column_value != '' %}{% if column.include_label %}<span class="source-column-label">{{ column.label }}</span>: {% endif %}{{ column_value }}{% endif %}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -136,7 +98,6 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% include 'pagination/pagination.html' %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,6 +1,7 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load appearance_tags %}
{% load common_tags %} {% load common_tags %}
{% load navigation_tags %} {% load navigation_tags %}
@@ -11,44 +12,16 @@
{% include 'appearance/no_results.html' %} {% include 'appearance/no_results.html' %}
</div> </div>
{% else %} {% else %}
<h4> {% include "appearance/list_header.html" %}
{% if page_obj %} {% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% if page_obj.paginator.num_pages != 1 %}
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
{% else %}
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
</h4>
<hr>
<div class="well center-block"> <div class="well center-block">
<div class="clearfix">
<div class="pull-right">
<form action="{% url 'common:multi_object_action_view' %}" class="form-multi-object-action" method="get">
{% if object_list %}
{% if not hide_multi_item_actions %}
{% get_multi_item_links_form object_list %}
{% endif %}
{% if multi_item_actions %}
<fieldset style="margin-top: -10px; margin-bottom: 10px;">
{{ multi_item_form }}
</fieldset>
{% endif %}
{% endif %}
</form>
</div>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
<tbody> <tbody>
{% if not hide_header %} {% if not hide_header %}
<tr> <tr>
{% if multi_item_actions %} {% if links_multi_menus_results %}
<th class="first"><input class="checkbox check-all" type="checkbox" /></th> <th class="first"></th>
{% endif %} {% endif %}
{% if not hide_object %} {% if not hide_object %}
@@ -58,30 +31,40 @@
{% if source_column %} {% if source_column %}
<th> <th>
{% if source_column.is_sortable %} {% if source_column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }} <a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}</a>
{% if source_column.get_sort_field == sort_field %} {% if source_column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %} {% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %} {% endif %}
</a>
{% else %} {% else %}
{{ source_column.label }} {{ source_column.label }}
{% endif %} {% endif %}
{% if source_column.help_text %}
<span data-toggle="tooltip" data-placement="bottom" title="{{ source_column.help_text }}">
{% get_icon icon_path='mayan.apps.navigation.icons.icon_source_column_help_text' %}
</span>
{% endif %}
</th> </th>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if not hide_columns %} {% if not hide_columns %}
{% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %} {% navigation_get_source_columns source=object_list exclude_identifier=True as source_columns %}
{% for column in source_columns %} {% for source_column in source_columns %}
<th> <th>
{% if column.is_sortable %} {% if source_column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=column %}">{{ column.label }} <a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}</a>
{% if column.get_sort_field == sort_field %} {% if source_column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %} {% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %} {% endif %}
</a>
{% else %} {% else %}
{{ column.label }} {{ source_column.label }}
{% endif %}
{% if source_column.help_text %}
<span data-toggle="tooltip" data-placement="bottom" title="{{ source_column.help_text }}">
{% get_icon icon_path='mayan.apps.navigation.icons.icon_source_column_help_text' %}
</span>
{% endif %} {% endif %}
</th> </th>
{% endfor %} {% endfor %}
@@ -99,9 +82,9 @@
{% for object in object_list %} {% for object in object_list %}
<tr> <tr>
{% if multi_item_actions %} {% if links_multi_menus_results %}
<td> <td>
<input type="checkbox" class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" value="" /> <input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
</td> </td>
{% endif %} {% endif %}
@@ -112,11 +95,7 @@
{% navigation_source_column_resolve column=source_column as column_value %} {% navigation_source_column_resolve column=source_column as column_value %}
{% if column_value %} {% if column_value %}
<td> <td>
{% if source_column.is_attribute_absolute_url or source_column.is_object_absolute_url %} {{ column_value }}
<a href="{% navigation_source_column_get_absolute_url source_column=source_column obj=object %}">{{ column_value }}</a>
{% else %}
{{ column_value }}
{% endif %}
</td> </td>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -170,7 +149,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% include 'pagination/pagination.html' %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@@ -1,4 +1,7 @@
<span class="fa-layers fa-fw" style="margin-right: 7px;"> <span class="fa-layers fa-fw" style="margin-right: 7px;">
{% if enable_shadow %}
<i class="{{ shadow_class }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3); stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
{% endif %}
{% for entry in data %} {% for entry in data %}
<i class="{{ entry.class }}" data-fa-transform="{{ entry.transform }}" data-fa-mask="{{ entry.mask }}"></i> <i class="{{ entry.class }}" data-fa-transform="{{ entry.transform }}" data-fa-mask="{{ entry.mask }}"></i>
{% endfor %} {% endfor %}

View File

@@ -1 +1,8 @@
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i> {% if enable_shadow %}
<span class="fa-layers fa-fw" >
<i class="fa fa-{{ symbol }}" data-fa-transform="right-1 down-2" style="color:rgba(0, 0, 0, 0.3);stroke: rgba(255, 255, 255, 0.3); stroke-width: 20;"></i>
<i class="fa fa-{{ symbol }}"></i>
</span>
{% else %}
<i class="fa fa-{{ symbol }}" style="padding-right: 5px; width: auto;"></i>
{% endif %}

View File

@@ -0,0 +1,28 @@
{% load i18n %}
{% load static %}
{% load common_tags %}
{% load navigation_tags %}
{% if object_list %}
<h4>
{% if page_obj %}
{% if page_obj.paginator.num_pages != 1 %}
{% blocktrans with page_obj.start_index as start and page_obj.end_index as end and page_obj.paginator.object_list|length as total and page_obj.number as page_number and page_obj.paginator.num_pages as total_pages %}Total ({{ start }} - {{ end }} out of {{ total }}) (Page {{ page_number }} of {{ total_pages }}){% endblocktrans %}
{% else %}
{% blocktrans with page_obj.paginator.object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
{% else %}
{% blocktrans with object_list|length as total %}Total: {{ total }}{% endblocktrans %}
{% endif %}
</h4>
<hr>
{% if not hide_multi_item_actions %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% endif %}
{% endif %}
<div class="clearfix">
{% include 'appearance/list_toolbar.html' %}
</div>

View File

@@ -0,0 +1,90 @@
{% load i18n %}
{% load common_tags %}
{% load navigation_tags %}
{% if is_paginated or links_multi_menus_results %}
<div class="well center-block toolbar">
{% endif %}
{% if links_multi_menus_results %}
<div class="pull-left">
<div class="btn-toolbar" role="toolbar" style="margin-right: 10px;">
<div class="btn-group">
<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>
</div>
</div>
</div>
{% endif %}
{% if is_paginated %}
<div class="pull-left">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group">
{% if page_obj.has_previous %}
<a class="btn btn-default btn-sm" href="?{{ page_obj.previous_page_number.querystring }}">&lsaquo;&lsaquo;</a>
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">&lsaquo;&lsaquo;</a>
{% endif %}
{% for page in page_obj.pages %}
{% if page %}
{% ifequal page page_obj.number %}
<a class="active btn btn-default btn-sm pagination-disabled" href="#">{{ page }}</a>
{% else %}
<a class="btn btn-default btn-sm" href="?{{ page.querystring }}">{{ page }}</a>
{% endifequal %}
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">...</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a class="btn btn-default btn-sm" href="?{{ page_obj.next_page_number.querystring }}">&rsaquo;&rsaquo;</a>
{% else %}
<a class="btn btn-default btn-sm disabled" href="#">&rsaquo;&rsaquo;</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
{% if links_multi_menus_results %}
<p class="pull-right" id="multi-item-title" style="line-height: 16px; padding-top: 8px;">{% 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;">
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
{% trans 'Bulk actions' %}
<span class="caret"></span>
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% 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 %}
{% endfor %}
{% if not forloop.last and link_group %}
<li class="divider"></li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
{% if is_paginated or links_multi_menus_results %}
<div class="clearfix"></div>
</div>
{% endif %}

View File

@@ -0,0 +1,70 @@
{% load i18n %}
{% load navigation_tags %}
{% load smart_settings_tags %}
{% load common_tags %}
{% load navigation_tags %}
{% spaceless %}
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
{% 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>
{% 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>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
{% endfor %}
</div>
{% endspaceless %}

View File

@@ -3,10 +3,11 @@
{% load navigation_tags %} {% load navigation_tags %}
{% load smart_settings_tags %} {% load smart_settings_tags %}
{% spaceless %}
<nav class="navbar navbar-default navbar-fixed-top"> <nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <button aria-expanded="false" aria-controls="navbar" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" type="button">
<span class="sr-only">{% trans 'Toggle navigation' %}</span> <span class="sr-only">{% trans 'Toggle navigation' %}</span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
@@ -14,9 +15,10 @@
</button> </button>
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a> <a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav navbar-right">
{% navigation_resolve_menu name='main' as topbar_menus_results %} {% navigation_resolve_menu name='topbar' as topbar_menus_results %}
{% for tobpar_menu_result in topbar_menus_results %} {% for tobpar_menu_result in topbar_menus_results %}
{% for link_group in tobpar_menu_result.link_groups %} {% for link_group in tobpar_menu_result.link_groups %}
{% for link in link_group.links %} {% for link in link_group.links %}
@@ -34,24 +36,8 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% get_menu_links name='main' 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 %}
{% endfor %}
{% endfor %}
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
{% endspaceless %}

View File

@@ -32,8 +32,11 @@
{% if appearance_type == 'plain' %} {% if appearance_type == 'plain' %}
{% block content_plain %}{% endblock %} {% block content_plain %}{% endblock %}
{% else %} {% else %}
<div id="menu-topbar">
{% include 'appearance/menu_topbar.html' %}
</div>
<div id="menu-main"> <div id="menu-main">
{% include 'appearance/main_menu.html' %} {% include 'appearance/menu_main.html' %}
</div> </div>
<div class="main"> <div class="main">
<div class="row zero-margin"> <div class="row zero-margin">
@@ -101,11 +104,18 @@
var app = new MayanApp({ var app = new MayanApp({
ajaxMenusOptions: [ ajaxMenusOptions: [
{ {
callback: MayanApp.updateNavbarState,
interval: 5000, interval: 5000,
menuSelector: '#menu-main', menuSelector: '#menu-main',
name: 'menu_main', name: 'menu_main',
url: '{% url "rest_api:template-detail" "menu_main" %}' url: '{% url "rest_api:template-detail" "menu_main" %}'
}, },
{
interval: 5000,
menuSelector: '#menu-topbar',
name: 'menu_topbar',
url: '{% url "rest_api:template-detail" "menu_topbar" %}'
},
] ]
}); });

View File

@@ -11,7 +11,7 @@
{% if page %} {% if page %}
{% ifequal page page_obj.number %} {% ifequal page page_obj.number %}
<li class="active"><a href="#">{{ page }}</a></li> <li class="active"><a class="pagination-disabled" href="#">{{ page }}</a></li>
{% else %} {% else %}
<li><a href="?{{ page.querystring }}">{{ page }}</a></li> <li><a href="?{{ page.querystring }}">{{ page }}</a></li>
{% endifequal %} {% endifequal %}

View File

@@ -7,6 +7,11 @@ from django.utils.translation import ugettext_lazy as _
register = Library() register = Library()
@register.simple_tag
def appearance_icon_render(icon_class, enable_shadow=False):
return icon_class.render(extra_context={'enable_shadow': enable_shadow})
@register.filter @register.filter
def get_choice_value(field): def get_choice_value(field):
try: try:

View File

@@ -17,7 +17,7 @@
{% motd %} {% motd %}
<div class="row"> <div class="row">
<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="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-6 col-lg-offset-3">
<div class="panel panel-primary"> <div class="panel panel-primary">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">&nbsp;</h3> <h3 class="panel-title">&nbsp;</h3>

View File

@@ -14,7 +14,6 @@ from django.utils.http import urlunquote_plus
from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.smart_settings.classes import Namespace from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.permissions import permission_user_edit from mayan.apps.user_management.permissions import permission_user_edit
from mayan.apps.user_management.tests.mixins import UserTestMixin
from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED
from ..settings import setting_maximum_session_length from ..settings import setting_maximum_session_length
@@ -262,7 +261,7 @@ class UserLoginTestCase(GenericViewTestCase):
self.assertEqual(response.redirect_chain, [(TEST_REDIRECT_URL, 302)]) self.assertEqual(response.redirect_chain, [(TEST_REDIRECT_URL, 302)])
class UserViewTestCase(UserTestMixin, UserPasswordViewTestMixin, GenericViewTestCase): class UserViewTestCase(UserPasswordViewTestMixin, GenericViewTestCase):
def test_user_set_password_view_no_access(self): def test_user_set_password_view_no_access(self):
self._create_test_user() self._create_test_user()

View File

@@ -4,7 +4,7 @@
{% if autoadmin_properties.account %} {% if autoadmin_properties.account %}
<div class="row"> <div class="row">
<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="col-xs-12 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-6 col-lg-offset-3">
<br> <br>
<div class="panel panel-primary"> <div class="panel panel-primary">
<div class="panel-heading"> <div class="panel-heading">

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-07-29 02:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cabinets', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='cabinet',
name='label',
field=models.CharField(help_text='A short text used to identify the cabinet.', max_length=128, verbose_name='Label'),
),
]

View File

@@ -32,7 +32,10 @@ class Cabinet(MPTTModel):
blank=True, db_index=True, null=True, on_delete=models.CASCADE, blank=True, db_index=True, null=True, on_delete=models.CASCADE,
related_name='children', to='self' related_name='children', to='self'
) )
label = models.CharField(max_length=128, verbose_name=_('Label')) label = models.CharField(
help_text=_('A short text used to identify the cabinet.'),
max_length=128, verbose_name=_('Label')
)
documents = models.ManyToManyField( documents = models.ManyToManyField(
blank=True, related_name='cabinets', to=Document, blank=True, related_name='cabinets', to=Document,
verbose_name=_('Documents') verbose_name=_('Documents')

View File

@@ -12,55 +12,62 @@ from .views import (
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView, CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
) )
urlpatterns = [ urlpatterns_cabinets = [
url( url(
regex=r'^list/$', view=CabinetListView.as_view(), name='cabinet_list' regex=r'^cabinets/$', view=CabinetListView.as_view(), name='cabinet_list'
), ),
url( url(
regex=r'^(?P<pk>\d+)/child/add/$', view=CabinetChildAddView.as_view(), regex=r'^cabinets/create/$', view=CabinetCreateView.as_view(),
name='cabinet_child_add'
),
url(
regex=r'^create/$', view=CabinetCreateView.as_view(),
name='cabinet_create' name='cabinet_create'
), ),
url( url(
regex=r'^(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(), regex=r'^cabinets/(?P<pk>\d+)/children/add/$', view=CabinetChildAddView.as_view(),
name='cabinet_edit' name='cabinet_child_add'
), ),
url( url(
regex=r'^(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(), regex=r'^cabinets/(?P<pk>\d+)/delete/$', view=CabinetDeleteView.as_view(),
name='cabinet_delete' name='cabinet_delete'
), ),
url( url(
regex=r'^(?P<pk>\d+)/$', view=CabinetDetailView.as_view(), regex=r'^cabinets/(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
name='cabinet_view' name='cabinet_edit'
), ),
url( url(
regex=r'^document/(?P<pk>\d+)/cabinet/add/$', regex=r'^cabinets/(?P<pk>\d+)/$', view=CabinetDetailView.as_view(),
name='cabinet_view'
),
]
urlpatterns_documents_cabinets = [
url(
regex=r'^documents/(?P<pk>\d+)/cabinets/add/$',
view=DocumentAddToCabinetView.as_view(), name='document_cabinet_add' view=DocumentAddToCabinetView.as_view(), name='document_cabinet_add'
), ),
url( url(
regex=r'^document/multiple/cabinet/add/$', regex=r'^documents/multiple/cabinets/add/$',
view=DocumentAddToCabinetView.as_view(), view=DocumentAddToCabinetView.as_view(),
name='document_multiple_cabinet_add' name='document_multiple_cabinet_add'
), ),
url( url(
regex=r'^document/(?P<pk>\d+)/cabinet/remove/$', regex=r'^documents/(?P<pk>\d+)/cabinets/remove/$',
view=DocumentRemoveFromCabinetView.as_view(), view=DocumentRemoveFromCabinetView.as_view(),
name='document_cabinet_remove' name='document_cabinet_remove'
), ),
url( url(
regex=r'^document/multiple/cabinet/remove/$', regex=r'^documents/multiple/cabinets/remove/$',
view=DocumentRemoveFromCabinetView.as_view(), view=DocumentRemoveFromCabinetView.as_view(),
name='multiple_document_cabinet_remove' name='multiple_document_cabinet_remove'
), ),
url( url(
regex=r'^document/(?P<pk>\d+)/cabinet/list/$', regex=r'^documents/(?P<pk>\d+)/cabinets/$',
view=DocumentCabinetListView.as_view(), name='document_cabinet_list' view=DocumentCabinetListView.as_view(), name='document_cabinet_list'
), ),
] ]
urlpatterns = []
urlpatterns.extend(urlpatterns_cabinets)
urlpatterns.extend(urlpatterns_documents_cabinets)
api_urls = [ api_urls = [
url( url(
regex=r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$', regex=r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',

View File

@@ -6,9 +6,12 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.classes import ModelPermission from mayan.apps.acls.classes import ModelPermission
from mayan.apps.common.apps import MayanAppConfig from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import menu_facet, menu_main, menu_secondary from mayan.apps.common.menus import (
menu_facet, menu_main, menu_multi_item, menu_secondary
)
from mayan.apps.dashboards.dashboards import dashboard_main from mayan.apps.dashboards.dashboards import dashboard_main
from mayan.apps.events.classes import ModelEventType from mayan.apps.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn
from .dashboard_widgets import DashboardWidgetTotalCheckouts from .dashboard_widgets import DashboardWidgetTotalCheckouts
from .events import ( from .events import (
@@ -17,8 +20,9 @@ from .events import (
) )
from .handlers import handler_check_new_version_creation from .handlers import handler_check_new_version_creation
from .links import ( from .links import (
link_check_in_document, link_check_out_document, link_check_out_info, link_check_in_document, link_check_in_document_multiple,
link_check_out_list link_check_out_document, link_check_out_document_multiple,
link_check_out_info, link_check_out_list
) )
from .methods import ( from .methods import (
method_check_in, method_get_check_out_info, method_get_check_out_state, method_check_in, method_get_check_out_info, method_get_check_out_state,
@@ -43,6 +47,8 @@ class CheckoutsApp(MayanAppConfig):
def ready(self): def ready(self):
super(CheckoutsApp, self).ready() super(CheckoutsApp, self).ready()
CheckedOutDocument = self.get_model(model_name='CheckedOutDocument')
DocumentCheckout = self.get_model(model_name='DocumentCheckout')
Document = apps.get_model( Document = apps.get_model(
app_label='documents', model_name='Document' app_label='documents', model_name='Document'
) )
@@ -76,6 +82,22 @@ class CheckoutsApp(MayanAppConfig):
permission_document_check_out_detail_view permission_document_check_out_detail_view
) )
) )
ModelPermission.register_inheritance(
model=DocumentCheckout, related='document'
)
SourceColumn(
attribute='get_user_display', include_label=True, order=99,
source=CheckedOutDocument
)
SourceColumn(
attribute='get_checkout_datetime', include_label=True, order=99,
source=CheckedOutDocument
)
SourceColumn(
attribute='get_checkout_expiration', include_label=True, order=99,
source=CheckedOutDocument
)
dashboard_main.add_widget( dashboard_main.add_widget(
widget=DashboardWidgetTotalCheckouts, order=-1 widget=DashboardWidgetTotalCheckouts, order=-1
@@ -85,6 +107,22 @@ class CheckoutsApp(MayanAppConfig):
links=(link_check_out_info,), sources=(Document,) links=(link_check_out_info,), sources=(Document,)
) )
menu_main.bind_links(links=(link_check_out_list,), position=98) menu_main.bind_links(links=(link_check_out_list,), position=98)
menu_multi_item.bind_links(
links=(
link_check_in_document_multiple,
), sources=(CheckedOutDocument,)
)
menu_multi_item.bind_links(
links=(
link_check_in_document_multiple,
link_check_out_document_multiple,
), sources=(Document,)
)
menu_multi_item.unbind_links(
links=(
link_check_out_document_multiple,
), sources=(CheckedOutDocument,)
)
menu_secondary.bind_links( menu_secondary.bind_links(
links=(link_check_out_document, link_check_in_document), links=(link_check_out_document, link_check_in_document),
sources=( sources=(

View File

@@ -38,16 +38,26 @@ link_check_out_document = Link(
args='object.pk', condition=is_not_checked_out, args='object.pk', condition=is_not_checked_out,
icon_class=icon_check_out_document, icon_class=icon_check_out_document,
permissions=(permission_document_check_out,), permissions=(permission_document_check_out,),
text=_('Check out document'), view='checkouts:check_out_document', text=_('Check out document'), view='checkouts:check_out_document'
)
link_check_out_document_multiple = Link(
icon_class=icon_check_out_document,
permissions=(permission_document_check_out,), text=_('Check out'),
view='checkouts:check_out_document_multiple'
) )
link_check_in_document = Link( link_check_in_document = Link(
args='object.pk', icon_class=icon_check_in_document, args='object.pk', icon_class=icon_check_in_document,
condition=is_checked_out, permissions=( condition=is_checked_out, permissions=(
permission_document_check_in, permission_document_check_in_override permission_document_check_in, permission_document_check_in_override
), text=_('Check in document'), view='checkouts:check_in_document', ), text=_('Check in document'), view='checkouts:check_in_document'
)
link_check_in_document_multiple = Link(
icon_class=icon_check_in_document,
permissions=(permission_document_check_in,), text=_('Check in'),
view='checkouts:check_in_document_multiple'
) )
link_check_out_info = Link( link_check_out_info = Link(
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=( args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
permission_document_check_out_detail_view, permission_document_check_out_detail_view,
), text=_('Check in/out'), view='checkouts:check_out_info', ), text=_('Check in/out'), view='checkouts:check_out_info'
) )

View File

@@ -6,6 +6,7 @@ from django.apps import apps
from django.db import models, transaction from django.db import models, transaction
from django.utils.timezone import now from django.utils.timezone import now
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from .events import ( from .events import (
@@ -14,10 +15,53 @@ from .events import (
) )
from .exceptions import DocumentNotCheckedOut from .exceptions import DocumentNotCheckedOut
from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN from .literals import STATE_CHECKED_OUT, STATE_CHECKED_IN
from .permissions import (
permission_document_check_in, permission_document_check_in_override
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DocumentCheckoutBusinessLogicManager(models.Manager):
def check_in_document(self, document, user=None):
queryset = document._meta.default_manager.filter(pk=document.pk)
return self.check_in_documents(queryset=queryset, user=user)
def check_in_documents(self, queryset, user=None):
if user:
user_document_checkouts = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in,
queryset=self.filter(user_id=user.pk, document__in=queryset),
user=user
)
others_document_checkouts = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in_override,
queryset=self.exclude(user_id=user.pk, document__in=queryset),
user=user
)
with transaction.atomic():
if user:
for checkout in user_document_checkouts:
event_document_check_in.commit(
actor=user, target=checkout.document
)
checkout.delete()
for checkout in others_document_checkouts:
event_document_forceful_check_in.commit(
actor=user, target=checkout.document
)
checkout.delete()
else:
for checkout in self.filter(document__in=queryset):
event_document_auto_check_in.commit(
target=checkout.document
)
checkout.delete()
class DocumentCheckoutManager(models.Manager): class DocumentCheckoutManager(models.Manager):
def are_document_new_versions_allowed(self, document, user=None): def are_document_new_versions_allowed(self, document, user=None):
try: try:
@@ -27,25 +71,6 @@ class DocumentCheckoutManager(models.Manager):
else: else:
return not check_out_info.block_new_version return not check_out_info.block_new_version
def check_in_document(self, document, user=None):
try:
document_check_out = self.model.objects.get(document=document)
except self.model.DoesNotExist:
raise DocumentNotCheckedOut
else:
with transaction.atomic():
if user:
if self.get_check_out_info(document=document).user != user:
event_document_forceful_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)
document_check_out.delete()
def check_in_expired_check_outs(self): def check_in_expired_check_outs(self):
for document in self.expired_check_outs(): for document in self.expired_check_outs():
document.check_in() document.check_in()
@@ -57,7 +82,11 @@ class DocumentCheckoutManager(models.Manager):
) )
def checked_out_documents(self): def checked_out_documents(self):
return Document.objects.filter( CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
return CheckedOutDocument.objects.filter(
pk__in=self.model.objects.values('document__id') pk__in=self.model.objects.values('document__id')
) )
@@ -74,7 +103,11 @@ class DocumentCheckoutManager(models.Manager):
return STATE_CHECKED_IN return STATE_CHECKED_IN
def expired_check_outs(self): def expired_check_outs(self):
expired_list = Document.objects.filter( CheckedOutDocument = apps.get_model(
app_label='checkouts', model_name='CheckedOutDocument'
)
expired_list = CheckedOutDocument.objects.filter(
pk__in=self.model.objects.filter( pk__in=self.model.objects.filter(
expiration_datetime__lte=now() expiration_datetime__lte=now()
).values_list('document__pk', flat=True) ).values_list('document__pk', flat=True)
@@ -83,9 +116,6 @@ class DocumentCheckoutManager(models.Manager):
return expired_list return expired_list
def get_by_natural_key(self, document_natural_key): def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try: try:
document = Document.objects.get_by_natural_key(document_natural_key) document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist: except Document.DoesNotExist:

View File

@@ -8,7 +8,7 @@ def method_check_in(self, user=None):
app_label='checkouts', model_name='DocumentCheckout' app_label='checkouts', model_name='DocumentCheckout'
) )
return DocumentCheckout.objects.check_in_document( return DocumentCheckout.business_logic.check_in_document(
document=self, user=user document=self, user=user
) )

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-25 04:52
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('documents', '0050_auto_20190725_0451'),
('checkouts', '0007_auto_20180310_1715'),
]
operations = [
migrations.CreateModel(
name='CheckedOutDocument',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('documents.document',),
),
]

View File

@@ -14,7 +14,10 @@ from mayan.apps.documents.models import Document
from .events import event_document_check_out from .events import event_document_check_out
from .exceptions import DocumentAlreadyCheckedOut from .exceptions import DocumentAlreadyCheckedOut
from .managers import DocumentCheckoutManager, NewVersionBlockManager from .managers import (
DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager,
NewVersionBlockManager
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -49,6 +52,7 @@ class DocumentCheckout(models.Model):
) )
objects = DocumentCheckoutManager() objects = DocumentCheckoutManager()
business_logic = DocumentCheckoutBusinessLogicManager()
class Meta: class Meta:
ordering = ('pk',) ordering = ('pk',)
@@ -81,13 +85,13 @@ class DocumentCheckout(models.Model):
natural_key.dependencies = ['documents.Document'] natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
new_checkout = not self.pk is_new = not self.pk
if not new_checkout or self.document.is_checked_out(): if not is_new or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut raise DocumentAlreadyCheckedOut
with transaction.atomic(): with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs) result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout: if is_new:
event_document_check_out.commit( event_document_check_out.commit(
actor=self.user, target=self.document actor=self.user, target=self.document
) )
@@ -119,3 +123,24 @@ class NewVersionBlock(models.Model):
def natural_key(self): def natural_key(self):
return self.document.natural_key() return self.document.natural_key()
natural_key.dependencies = ['documents.Document'] natural_key.dependencies = ['documents.Document']
class CheckedOutDocument(Document):
class Meta:
proxy = True
def get_user_display(self):
check_out_info = self.get_check_out_info()
return check_out_info.user.get_full_name() or check_out_info.user
get_user_display.short_description = _('User')
def get_checkout_datetime(self):
return self.get_check_out_info().checkout_datetime
get_checkout_datetime.short_description = _('Checkout time and date')
def get_checkout_expiration(self):
return self.get_check_out_info().expiration_datetime
get_checkout_expiration.short_description = _('Checkout expiration')

View File

@@ -4,13 +4,19 @@ import datetime
from django.utils.timezone import now from django.utils.timezone import now
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.common.tests.utils import as_id_list
from ..models import DocumentCheckout from ..models import DocumentCheckout
class DocumentCheckoutTestMixin(object): class DocumentCheckoutTestMixin(object):
_test_document_check_out_seconds = 0.1 _test_document_check_out_seconds = 0.1
def _check_out_test_document(self, user=None): def _check_out_test_document(self, document=None, user=None):
if not document:
document = self.test_document
if not user: if not user:
user = self._test_case_user user = self._test_case_user
@@ -19,7 +25,61 @@ class DocumentCheckoutTestMixin(object):
) )
self.test_check_out = DocumentCheckout.objects.check_out_document( self.test_check_out = DocumentCheckout.objects.check_out_document(
block_new_version=True, document=self.test_document, block_new_version=True, document=document,
expiration_datetime=self._check_out_expiration_datetime, expiration_datetime=self._check_out_expiration_datetime,
user=user user=user
) )
class DocumentCheckoutViewTestMixin(object):
def _request_test_document_check_in_get_view(self):
return self.get(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_multiple_check_in_post_view(self):
return self.post(
viewname='checkouts:check_in_document_multiple', data={
'id_list': as_id_list(items=self.test_documents)
}
)
def _request_test_document_check_out_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
'pk': self.test_document.pk
}, data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2
}
)
def _request_test_document_multiple_check_out_post_view(self):
return self.post(
viewname='checkouts:check_out_document_multiple', data={
'block_new_version': True,
'expiration_datetime_0': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_1': 2,
'id_list': as_id_list(items=self.test_documents)
}
)
def _request_test_document_check_out_detail_view(self):
return self.get(
viewname='checkouts:check_out_info', kwargs={
'pk': self.test_document.pk
}
)
def _request_test_document_check_out_list_view(self):
return self.get(viewname='checkouts:check_out_list')

View File

@@ -65,7 +65,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
force_text(self.test_document.uuid) force_text(self.test_document.uuid)
) )
def _request_document_checkout_view(self): def _request_test_document_check_out_view(self):
return self.post( return self.post(
viewname='rest_api:checkout-document-list', data={ viewname='rest_api:checkout-document-list', data={
'document_pk': self.test_document.pk, 'document_pk': self.test_document.pk,
@@ -74,7 +74,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
) )
def test_document_checkout_no_access(self): def test_document_checkout_no_access(self):
response = self._request_document_checkout_view() response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(DocumentCheckout.objects.count(), 0) self.assertEqual(DocumentCheckout.objects.count(), 0)
@@ -82,7 +82,7 @@ class CheckoutsAPITestCase(DocumentCheckoutTestMixin, DocumentTestMixin, BaseAPI
def test_document_checkout_with_access(self): def test_document_checkout_with_access(self):
self.grant_access(permission=permission_document_check_out, obj=self.test_document) self.grant_access(permission=permission_document_check_out, obj=self.test_document)
response = self._request_document_checkout_view() response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual( self.assertEqual(

View File

@@ -7,8 +7,7 @@ from mayan.apps.documents.tests import GenericDocumentTestCase, DocumentTestMixi
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from ..exceptions import ( from ..exceptions import (
DocumentAlreadyCheckedOut, DocumentNotCheckedOut, DocumentAlreadyCheckedOut, NewDocumentVersionNotAllowed
NewDocumentVersionNotAllowed
) )
from ..models import DocumentCheckout, NewVersionBlock from ..models import DocumentCheckout, NewVersionBlock
@@ -49,10 +48,6 @@ class DocumentCheckoutTestCase(DocumentCheckoutTestMixin, GenericDocumentTestCas
block_new_version=True block_new_version=True
) )
def test_checkin_without_checkout(self):
with self.assertRaises(DocumentNotCheckedOut):
self.test_document.check_in()
def test_auto_check_in(self): def test_auto_check_in(self):
self._check_out_test_document() self._check_out_test_document()

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.sources.links import link_document_version_upload from mayan.apps.sources.links import link_document_version_upload
@@ -12,64 +11,53 @@ from ..permissions import (
permission_document_check_out, permission_document_check_out_detail_view permission_document_check_out, permission_document_check_out_detail_view
) )
from .mixins import DocumentCheckoutTestMixin from .mixins import DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin
class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentViewTestCase): class DocumentCheckoutViewTestCase(
def _request_document_check_in_get_view(self): DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
return self.get( GenericDocumentViewTestCase
viewname='checkouts:check_in_document', kwargs={ ):
'pk': self.test_document.pk def test_document_check_in_get_view_no_permission(self):
}
)
def test_check_in_document_get_view_no_permission(self):
self._check_out_test_document() self._check_out_test_document()
response = self._request_document_check_in_get_view() response = self._request_test_document_check_in_get_view()
self.assertContains( self.assertNotContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=404
) )
self.assertTrue(self.test_document.is_checked_out()) self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_get_view_with_access(self): def test_document_check_in_get_view_with_access(self):
self._check_out_test_document() self._check_out_test_document()
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_document_check_in obj=self.test_document, permission=permission_document_check_in
) )
response = self._request_document_check_in_get_view() response = self._request_test_document_check_in_get_view()
self.assertContains( self.assertContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )
self.assertTrue(self.test_document.is_checked_out()) self.assertTrue(self.test_document.is_checked_out())
def _request_document_check_in_post_view(self): def test_document_check_in_post_view_no_permission(self):
return self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
def test_check_in_document_post_view_no_permission(self):
self._check_out_test_document() self._check_out_test_document()
response = self._request_document_check_in_post_view() response = self._request_test_document_check_in_post_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_document.is_checked_out()) self.assertTrue(self.test_document.is_checked_out())
def test_check_in_document_post_view_with_access(self): def test_document_check_in_post_view_with_access(self):
self._check_out_test_document() self._check_out_test_document()
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_document_check_in obj=self.test_document, permission=permission_document_check_in
) )
response = self._request_document_check_in_post_view() response = self._request_test_document_check_in_post_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out()) self.assertFalse(self.test_document.is_checked_out())
@@ -79,24 +67,93 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
) )
) )
def _request_document_checkout_view(self): def test_document_multiple_check_in_post_view_no_permission(self):
return self.post( # Upload second document
viewname='checkouts:check_out_document', kwargs={ self.upload_document()
'pk': self.test_document.pk
}, data={ self._check_out_test_document(document=self.test_documents[0])
'expiration_datetime_0': 2, self._check_out_test_document(document=self.test_documents[1])
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
'block_new_version': True response = self._request_test_document_multiple_check_in_post_view()
} self.assertEqual(response.status_code, 404)
self.assertTrue(self.test_documents[0].is_checked_out())
self.assertTrue(self.test_documents[1].is_checked_out())
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
) )
def test_check_out_document_view_no_permission(self): def test_document_multiple_check_in_post_view_with_document_0_access(self):
response = self._request_document_checkout_view() # Upload second document
self.assertEqual(response.status_code, 403) self.upload_document()
self._check_out_test_document(document=self.test_documents[0])
self._check_out_test_document(document=self.test_documents[1])
self.grant_access(
obj=self.test_documents[0], permission=permission_document_check_in
)
response = self._request_test_document_multiple_check_in_post_view()
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_documents[0].is_checked_out())
self.assertTrue(self.test_documents[1].is_checked_out())
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_multiple_check_in_post_view_with_access(self):
# Upload second document
self.upload_document()
self._check_out_test_document(document=self.test_documents[0])
self._check_out_test_document(document=self.test_documents[1])
self.grant_access(
obj=self.test_documents[0], permission=permission_document_check_in
)
self.grant_access(
obj=self.test_documents[1], permission=permission_document_check_in
)
response = self._request_test_document_multiple_check_in_post_view()
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_documents[0].is_checked_out())
self.assertFalse(self.test_documents[1].is_checked_out())
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_check_out_view_no_permission(self):
response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, 404)
self.assertFalse(self.test_document.is_checked_out()) self.assertFalse(self.test_document.is_checked_out())
def test_check_out_document_view_with_access(self): def test_document_check_out_view_with_access(self):
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_document_check_out obj=self.test_document, permission=permission_document_check_out
) )
@@ -105,28 +162,117 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_check_out_detail_view permission=permission_document_check_out_detail_view
) )
response = self._request_document_checkout_view() response = self._request_test_document_check_out_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out()) self.assertTrue(self.test_document.is_checked_out())
def _request_check_out_detail_view(self): def test_document_multiple_check_out_post_view_no_permission(self):
return self.get( # Upload second document
viewname='checkouts:check_out_info', kwargs={ self.upload_document()
'pk': self.test_document.pk
} self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
) )
def test_checkout_detail_view_no_permission(self): response = self._request_test_document_multiple_check_out_post_view()
self.assertEqual(response.status_code, 404)
self.assertFalse(self.test_documents[0].is_checked_out())
self.assertFalse(self.test_documents[1].is_checked_out())
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_multiple_check_out_post_view_with_document_0_access(self):
# Upload second document
self.upload_document()
self.grant_access(
obj=self.test_documents[0], permission=permission_document_check_out
)
self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
)
response = self._request_test_document_multiple_check_out_post_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_documents[0].is_checked_out())
self.assertFalse(self.test_documents[1].is_checked_out())
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertFalse(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_multiple_check_out_post_view_with_access(self):
# Upload second document
self.upload_document()
self.grant_access(
obj=self.test_documents[0], permission=permission_document_check_out
)
self.grant_access(
obj=self.test_documents[1], permission=permission_document_check_out
)
self.grant_access(
obj=self.test_documents[0],
permission=permission_document_check_out_detail_view
)
self.grant_access(
obj=self.test_documents[1],
permission=permission_document_check_out_detail_view
)
response = self._request_test_document_multiple_check_out_post_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_documents[0].is_checked_out())
self.assertTrue(self.test_documents[1].is_checked_out())
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[0]
)
)
self.assertTrue(
DocumentCheckout.objects.is_checked_out(
document=self.test_documents[1]
)
)
def test_document_check_out_detail_view_no_permission(self):
self._check_out_test_document() self._check_out_test_document()
response = self._request_check_out_detail_view() response = self._request_test_document_check_out_detail_view()
self.assertNotContains( self.assertNotContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404 response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=404
) )
def test_checkout_detail_view_with_access(self): def test_document_check_out_detail_view_with_access(self):
self._check_out_test_document() self._check_out_test_document()
self.grant_access( self.grant_access(
@@ -134,15 +280,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_check_out_detail_view permission=permission_document_check_out_detail_view
) )
response = self._request_check_out_detail_view() response = self._request_test_document_check_out_detail_view()
self.assertContains( self.assertContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200 response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200
) )
def _request_check_out_list_view(self): def test_document_checkout_list_view_no_permission(self):
return self.get(viewname='checkouts:check_out_list')
def test_checkout_list_view_no_permission(self):
self._check_out_test_document() self._check_out_test_document()
self.grant_access( self.grant_access(
@@ -150,12 +293,12 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_view permission=permission_document_view
) )
response = self._request_check_out_list_view() response = self._request_test_document_check_out_list_view()
self.assertNotContains( self.assertNotContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )
def test_checkout_list_view_with_access(self): def test_document_checkout_list_view_with_access(self):
self._check_out_test_document() self._check_out_test_document()
self.grant_access( self.grant_access(
@@ -167,12 +310,54 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
permission=permission_document_view permission=permission_document_view
) )
response = self._request_check_out_list_view() response = self._request_test_document_check_out_list_view()
self.assertContains( self.assertContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )
def test_document_new_version_after_check_out(self): def test_document_check_in_forcefull_view_no_permission(self):
# Gitlab issue #237
# Forcefully checking in a document by a user without adequate
# permissions throws out an error
self._create_test_user()
self._check_out_test_document(user=self.test_user)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_forcefull_view_with_access(self):
self._create_test_user()
self._check_out_test_document(user=self.test_user)
self.grant_access(
obj=self.test_document,
permission=permission_document_check_in_override
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())
class NewVersionBlockViewTestCase(
DocumentCheckoutTestMixin, DocumentCheckoutViewTestMixin,
GenericDocumentViewTestCase):
def test_document_check_out_new_version(self):
""" """
Gitlab issue #231 Gitlab issue #231
User shown option to upload new version of a document even though it User shown option to upload new version of a document even though it
@@ -205,49 +390,8 @@ class DocumentCheckoutViewTestCase(DocumentCheckoutTestMixin, GenericDocumentVie
# Needed by the url view resolver # Needed by the url view resolver
response.context.current_app = None response.context.current_app = None
resolved_link = link_document_version_upload.resolve(context=response.context) resolved_link = link_document_version_upload.resolve(
context=response.context
)
self.assertEqual(resolved_link, None) self.assertEqual(resolved_link, None)
def test_forcefull_check_in_document_view_no_permission(self):
# Gitlab issue #237
# Forcefully checking in a document by a user without adequate
# permissions throws out an error
self._create_test_case_superuser()
self._check_out_test_document(user=self._test_case_superuser)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertContains(
response=response, text='Insufficient permissions', status_code=403
)
self.assertTrue(self.test_document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self):
self._create_test_case_superuser()
self._check_out_test_document(user=self._test_case_superuser)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in
)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in_override
)
response = self.post(
viewname='checkouts:check_in_document', kwargs={
'pk': self.test_document.pk
}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(self.test_document.is_checked_out())

View File

@@ -4,25 +4,34 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import ( from .views import (
CheckoutDocumentView, CheckoutDetailView, CheckoutListView, DocumentCheckinView, DocumentCheckoutDetailView, DocumentCheckoutView,
DocumentCheckinView DocumentCheckoutListView
) )
urlpatterns = [ urlpatterns = [
url( url(
regex=r'^list/$', view=CheckoutListView.as_view(), name='check_out_list' regex=r'^documents/$', view=DocumentCheckoutListView.as_view(),
name='check_out_list'
), ),
url( url(
regex=r'^(?P<pk>\d+)/check/out/$', view=CheckoutDocumentView.as_view(), regex=r'^documents/(?P<pk>\d+)/check_in/$', view=DocumentCheckinView.as_view(),
name='check_out_document'
),
url(
regex=r'^(?P<pk>\d+)/check/in/$', view=DocumentCheckinView.as_view(),
name='check_in_document' name='check_in_document'
), ),
url( url(
regex=r'^(?P<pk>\d+)/check/info/$', view=CheckoutDetailView.as_view(), regex=r'^documents/multiple/check_in/$',
name='check_out_info' name='check_in_document_multiple', view=DocumentCheckinView.as_view()
),
url(
regex=r'^documents/(?P<pk>\d+)/check_out/$', view=DocumentCheckoutView.as_view(),
name='check_out_document'
),
url(
regex=r'^documents/multiple/check_out/$',
name='check_out_document_multiple', view=DocumentCheckoutView.as_view()
),
url(
regex=r'^documents/(?P<pk>\d+)/checkout/info/$',
view=DocumentCheckoutDetailView.as_view(), name='check_out_info'
), ),
] ]

View File

@@ -1,20 +1,16 @@
from __future__ import absolute_import, unicode_literals 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.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.acls.models import AccessControlList
from mayan.apps.common.generics import ( 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.models import Document
from mayan.apps.documents.views import DocumentListView from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
from .icons import icon_check_out_info from .icons import icon_check_out_info
from .models import DocumentCheckout from .models import DocumentCheckout
@@ -24,159 +20,124 @@ from .permissions import (
) )
class DocumentCheckinView(ConfirmView): class DocumentCheckinView(MultipleObjectConfirmActionView):
def get_extra_context(self): error_message = 'Unable to check in document "%(instance)s". %(exception)s'
document = self.get_object() model = Document
pk_url_kwarg = 'pk'
context = { success_message_singular = '%(count)d document checked in.'
'object': document, success_message_plural = '%(count)d documents checked in.'
}
if document.get_check_out_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(
viewname='checkouts:check_out_info', kwargs={
'pk': self.get_object().pk
}
)
def view_action(self):
document = self.get_object()
if document.get_check_out_info().user == self.request.user:
AccessControlList.objects.check_access(
obj=document, permissions=(permission_document_check_in,),
user=self.request.user
)
else:
AccessControlList.objects.check_access(
obj=document,
permissions=(permission_document_check_in_override,),
user=self.request.user
)
try:
document.check_in(user=self.request.user)
except DocumentNotCheckedOut:
messages.error(
message=_('Document has not been checked out.'),
request=self.request
)
else:
messages.success(
message=_(
'Document "%s" checked in successfully.'
) % document, request=self.request
)
class CheckoutDocumentView(SingleObjectCreateView):
form_class = DocumentCheckoutForm
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=self.document, permissions=(permission_document_check_out,),
user=request.user
)
return super(
CheckoutDocumentView, self
).dispatch(request, *args, **kwargs)
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(
message=_('Document already checked out.'),
request=self.request
)
else:
messages.success(
message=_(
'Document "%s" checked out successfully.'
) % self.document, request=self.request
)
return HttpResponseRedirect(redirect_to=self.get_success_url())
def get_extra_context(self): def get_extra_context(self):
return { queryset = self.get_object_list()
'object': self.document,
'title': _('Check out document: %s') % self.document result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
} }
def get_post_action_redirect(self): if queryset.count() == 1:
return reverse( result.update(
viewname='checkouts:check_out_info', kwargs={ {
'pk': self.document.pk 'object': queryset.first(),
} 'title': _(
) 'Check in document: %s'
) % queryset.first()
}
)
return result
class CheckoutListView(DocumentListView): def get_post_object_action_url(self):
def get_document_queryset(self): if self.action_count == 1:
return AccessControlList.objects.restrict_queryset( return reverse(
permission=permission_document_check_out_detail_view, viewname='checkouts:document_checkout_info',
queryset=DocumentCheckout.objects.checked_out_documents(), kwargs={'pk': self.action_id_list[0]}
)
else:
super(DocumentCheckinView, self).get_post_action_redirect()
def get_source_queryset(self):
# object_permission is None to disable restricting queryset mixin
# and restrict the queryset ourselves from two permissions
source_queryset = super(DocumentCheckinView, self).get_source_queryset()
check_in_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in, queryset=source_queryset,
user=self.request.user user=self.request.user
) )
def get_extra_context(self): check_in_override_queryset = AccessControlList.objects.restrict_queryset(
context = super(CheckoutListView, self).get_extra_context() permission=permission_document_check_in_override,
context.update( queryset=source_queryset, user=self.request.user
{ )
'extra_columns': (
{ return check_in_queryset | check_in_override_queryset
'name': _('User'),
'attribute': encapsulate( def object_action(self, form, instance):
lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().user DocumentCheckout.business_logic.check_in_document(
) document=instance, user=self.request.user
},
{
'name': _('Checkout time and date'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().checkout_datetime
)
},
{
'name': _('Checkout expiration'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().expiration_datetime
)
},
),
'no_results_icon': icon_check_out_info,
'no_results_text': _(
'Checking out a document blocks certain document '
'operations for a predetermined amount of '
'time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Documents checked out'),
}
) )
return context
class CheckoutDetailView(SingleObjectDetailView): class DocumentCheckoutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckoutForm
model = Document
object_permission = permission_document_check_out
pk_url_kwarg = 'pk'
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 result
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'pk': self.action_id_list[0]}
)
else:
super(DocumentCheckoutView, self).get_post_action_redirect()
def object_action(self, form, instance):
DocumentCheckout.objects.check_out_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 form_class = DocumentCheckoutDefailForm
model = Document model = Document
object_permission = permission_document_check_out_detail_view object_permission = permission_document_check_out_detail_view
@@ -188,3 +149,27 @@ class CheckoutDetailView(SingleObjectDetailView):
'Check out details for document: %s' 'Check out details for document: %s'
) % self.object ) % self.object
} }
class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_check_out_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
def get_extra_context(self):
context = super(DocumentCheckoutListView, self).get_extra_context()
context.update(
{
'no_results_icon': icon_check_out_info,
'no_results_text': _(
'Checking out a document, blocks certain operations '
'for a predetermined amount of time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Checked out documents'),
}
)
return context

View File

@@ -32,8 +32,8 @@ class SplitTimeDeltaWidget(forms.widgets.MultiWidget):
return (None, None) return (None, None)
def value_from_datadict(self, querydict, files, name): def value_from_datadict(self, querydict, files, name):
unit = querydict.get('{}_1'.format(name)) unit = querydict.get('{}_0'.format(name))
period = querydict.get('{}_0'.format(name)) period = querydict.get('{}_1'.format(name))
if not unit or not period: if not unit or not period:
return now() return now()

View File

@@ -27,9 +27,7 @@ from .links import (
) )
from .literals import MESSAGE_SQLITE_WARNING from .literals import MESSAGE_SQLITE_WARNING
from .menus import ( from .menus import menu_about, menu_secondary, menu_topbar, menu_user
menu_about, menu_main, menu_secondary, menu_user
)
from .settings import ( from .settings import (
setting_auto_logging, setting_production_error_log_path, setting_auto_logging, setting_production_error_log_path,
setting_production_error_logging setting_production_error_logging
@@ -97,7 +95,10 @@ class CommonApp(MayanAppConfig):
) )
Template( Template(
name='menu_main', template_name='appearance/main_menu.html' name='menu_main', template_name='appearance/menu_main.html'
)
Template(
name='menu_topbar', template_name='appearance/menu_topbar.html'
) )
menu_user.bind_links( menu_user.bind_links(
@@ -112,7 +113,7 @@ class CommonApp(MayanAppConfig):
) )
) )
menu_main.bind_links(links=(menu_about, menu_user,), position=99) menu_topbar.bind_links(links=(menu_about, menu_user,), position=99)
menu_secondary.bind_links( menu_secondary.bind_links(
links=(link_object_error_list_clear,), sources=( links=(link_object_error_list_clear,), sources=(
'common:object_error_list', 'common:object_error_list',

View File

@@ -61,102 +61,9 @@ PythonDependency(
SOFTWARE. SOFTWARE.
''', module=__name__, name='PyYAML', version_string='==5.1.1' ''', module=__name__, name='PyYAML', version_string='==5.1.1'
) )
PythonDependency(
copyright_text='''
Copyright (c) 2015 Ask Solem & contributors. All rights reserved.
Copyright (c) 2012-2014 GoPivotal, Inc. All rights reserved.
Copyright (c) 2009, 2010, 2011, 2012 Ask Solem, and individual contributors. All rights reserved.
Celery is licensed under The BSD License (3 Clause, also known as
the new BSD license). The license is an OSI approved Open Source
license and is GPL-compatible(1).
The license text can also be found here:
http://www.opensource.org/licenses/BSD-3-Clause
License
=======
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ask Solem, nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Ask Solem OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Documentation License
=====================
The documentation portion of Celery (the rendered contents of the
"docs" directory of a software distribution or checkout) is supplied
under the Creative Commons Attribution-Noncommercial-Share Alike 3.0
United States License as described by
http://creativecommons.org/licenses/by-nc-sa/3.0/us/
Footnotes
=========
(1) A GPL-compatible license makes it possible to
combine Celery with other software that is released
under the GPL, it does not mean that we're distributing
Celery under the GPL license. The BSD license, unlike the GPL,
let you distribute a modified version without making your
changes open source.
''', module=__name__, name='celery', version_string='==3.1.24'
)
PythonDependency(
copyright_text='''
Copyright (c) 2012-2013 GoPivotal, Inc. All Rights Reserved.
Copyright (c) 2009-2012 Ask Solem. All Rights Reserved.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
Neither the name of Ask Solem nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
''', module=__name__, name='django-celery', version_string='==3.2.1'
)
PythonDependency( PythonDependency(
module=__name__, name='django-downloadview', version_string='==1.9' module=__name__, name='django-downloadview', version_string='==1.9'
) )
PythonDependency(
module=__name__, name='django-environ', version_string='==0.4.5'
)
PythonDependency( PythonDependency(
module=__name__, name='django-formtools', version_string='==2.1' module=__name__, name='django-formtools', version_string='==2.1'
) )
@@ -383,6 +290,10 @@ PythonDependency(
module=__name__, environment=environment_development, name='Werkzeug', module=__name__, environment=environment_development, name='Werkzeug',
version_string='==0.15.4' version_string='==0.15.4'
) )
PythonDependency(
module=__name__, environment=environment_development, name='devpi-server',
version_string='==5.0.0'
)
PythonDependency( PythonDependency(
environment=environment_development, module=__name__, environment=environment_development, module=__name__,
name='django-debug-toolbar', version_string='==1.11' name='django-debug-toolbar', version_string='==1.11'

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from django.http import QueryDict from django.http import QueryDict
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.six import PY3
class URL(object): class URL(object):
@@ -20,9 +21,7 @@ class URL(object):
def to_string(self): def to_string(self):
if self._args.keys(): if self._args.keys():
query = force_bytes( query = '?{}'.format(self._args.urlencode())
'?{}'.format(self._args.urlencode())
)
else: else:
query = '' query = ''
@@ -31,6 +30,9 @@ class URL(object):
else: else:
path = '' path = ''
result = force_bytes('{}{}'.format(path, query)) result = '{}{}'.format(path, query)
return result if PY3:
return result
else:
return force_bytes(result)

View File

@@ -35,8 +35,14 @@ icon_menu_about = Icon(
icon_menu_user = Icon( icon_menu_user = Icon(
driver_name='fontawesome', symbol='user-circle' driver_name='fontawesome', symbol='user-circle'
) )
icon_object_error_list_with_icon = Icon( icon_object_errors = Icon(
driver_name='fontawesome', symbol='lock' driver_name='fontawesome', symbol='exclamation-triangle'
)
icon_object_error_list = Icon(
driver_name='fontawesome', symbol='exclamation-triangle'
)
icon_object_error_list_clear = Icon(
driver_name='fontawesome', symbol='times'
) )
icon_ok = Icon( icon_ok = Icon(
driver_name='fontawesome', symbol='check' driver_name='fontawesome', symbol='check'

View File

@@ -8,8 +8,8 @@ from mayan.apps.navigation.classes import Link
from .icons import ( from .icons import (
icon_about, icon_current_user_locale_profile_details, icon_about, icon_current_user_locale_profile_details,
icon_current_user_locale_profile_edit, icon_documentation, icon_current_user_locale_profile_edit, icon_documentation,
icon_forum, icon_license, icon_object_error_list_with_icon, icon_forum, icon_license, icon_setup, icon_source_code, icon_support,
icon_setup, icon_source_code, icon_support, icon_tools icon_tools
) )
from .permissions_runtime import permission_error_log_view from .permissions_runtime import permission_error_log_view
@@ -50,21 +50,17 @@ link_documentation = Link(
text=_('Documentation'), url='https://docs.mayan-edms.com' text=_('Documentation'), url='https://docs.mayan-edms.com'
) )
link_object_error_list = Link( link_object_error_list = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
kwargs=get_kwargs_factory('resolved_object'), kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Errors'), permissions=(permission_error_log_view,), text=_('Errors'),
view='common:object_error_list', view='common:object_error_list',
) )
link_object_error_list_clear = Link( link_object_error_list_clear = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
kwargs=get_kwargs_factory('resolved_object'), kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'), permissions=(permission_error_log_view,), text=_('Clear all'),
view='common:object_error_list_clear', view='common:object_error_list_clear',
) )
link_object_error_list_with_icon = Link(
kwargs=get_kwargs_factory('resolved_object'),
icon_class=icon_object_error_list_with_icon,
permissions=(permission_error_log_view,), text=_('Errors'),
view='common:error_list',
)
link_forum = Link( link_forum = Link(
icon_class=icon_forum, tags='new_window', text=_('Forum'), icon_class=icon_forum, tags='new_window', text=_('Forum'),
url='https://forum.mayan-edms.com' url='https://forum.mayan-edms.com'

View File

@@ -1,107 +0,0 @@
from __future__ import unicode_literals
import errno
import os
import warnings
from pathlib2 import Path
from django.conf import settings
from django.core import management
from django.core.management.base import CommandError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.models import DocumentType
from mayan.apps.storage.utils import fs_cleanup
from ...literals import MESSAGE_DEPRECATION_WARNING
from ...warnings import DeprecationWarning
CONVERTDB_FOLDER = 'convertdb'
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'
class Command(management.BaseCommand):
help = 'Convert from a database backend to another one.'
def __init__(self, *args, **kwargs):
warnings.warn(
category=DeprecationWarning,
message=force_text(MESSAGE_DEPRECATION_WARNING)
)
super(Command, self).__init__(*args, **kwargs)
def add_arguments(self, parser):
parser.add_argument(
'args', metavar='app_label[.ModelName]', nargs='*',
help=_(
'Restricts dumped data to the specified app_label or '
'app_label.ModelName.'
)
)
parser.add_argument(
'--from', action='store', default='default', dest='from',
help=_(
'The database from which data will be exported. If omitted '
'the database named "default" will be used.'
),
)
parser.add_argument(
'--to', action='store', default='default', dest='to',
help=_(
'The database to which data will be imported. If omitted '
'the database named "default" will be used.'
),
)
parser.add_argument(
'--force', action='store_true', dest='force',
help=_(
'Force the conversion of the database even if the receiving '
'database is not empty.'
),
)
def handle(self, *app_labels, **options):
# Create the media/convertdb folder
convertdb_folder_path = force_text(
Path(
settings.MEDIA_ROOT, CONVERTDB_FOLDER
)
)
try:
os.makedirs(convertdb_folder_path)
except OSError as exception:
if exception.errno == errno.EEXIST:
pass
convertdb_file_path = force_text(
Path(
convertdb_folder_path, CONVERTDB_OUTPUT_FILENAME
)
)
management.call_command(command_name='purgeperiodictasks')
management.call_command(
'dumpdata', *app_labels, all=True,
database=options['from'], natural_primary=True,
natural_foreign=True, output=convertdb_file_path,
interactive=False, format='json'
)
if DocumentType.objects.using(options['to']).count() and not options['force']:
fs_cleanup(convertdb_file_path)
raise CommandError(
'There is existing data in the database that will be '
'used for the import. If you proceed with the conversion '
'you might lose data. Please check your settings.'
)
management.call_command(
'loaddata', convertdb_file_path, database=options['to'], interactive=False,
verbosity=3
)
fs_cleanup(convertdb_file_path)

View File

@@ -28,8 +28,8 @@ class Command(management.BaseCommand):
) )
parser.add_argument( parser.add_argument(
'--no-javascript', action='store_true', dest='no_javascript', '--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download the JavaScript dependencies.', help='Don\'t download dependencies.',
) )
def initialize_system(self, force=False): def initialize_system(self, force=False):
@@ -88,9 +88,9 @@ class Command(management.BaseCommand):
self.initialize_system(force=options.get('force', False)) self.initialize_system(force=options.get('force', False))
pre_initial_setup.send(sender=self) pre_initial_setup.send(sender=self)
if not options.get('no_javascript', False): if not options.get('no_dependencies', False):
management.call_command( management.call_command(
command_name='installjavascript', interactive=False command_name='installdependencies', interactive=False
) )
management.call_command( management.call_command(

View File

@@ -1,10 +0,0 @@
from __future__ import unicode_literals
SETTING_FILE_TEMPLATE = '''
from __future__ import absolute_import, unicode_literals
from .base import *
SECRET_KEY = '{0}'
'''

View File

@@ -11,8 +11,8 @@ class Command(management.BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--no-javascript', action='store_true', dest='no_javascript', '--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download the JavaScript dependencies.', help='Don\'t download dependencies.',
) )
def handle(self, *args, **options): def handle(self, *args, **options):
@@ -25,9 +25,9 @@ class Command(management.BaseCommand):
) )
) )
if not options.get('no_javascript', False): if not options.get('no_dependencies', False):
management.call_command( management.call_command(
command_name='installjavascript', interactive=False command_name='installdependencies', interactive=False
) )
try: try:

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.core import management from django.core import management
from djcelery.models import IntervalSchedule, PeriodicTask from django_celery_beat.models import IntervalSchedule, PeriodicTask
class Command(management.BaseCommand): class Command(management.BaseCommand):

View File

@@ -17,6 +17,7 @@ menu_object = Menu(label=_('Actions'), name='object')
menu_secondary = Menu(label=_('Secondary'), name='secondary') menu_secondary = Menu(label=_('Secondary'), name='secondary')
menu_setup = Menu(name='setup') menu_setup = Menu(name='setup')
menu_tools = Menu(name='tools') menu_tools = Menu(name='tools')
menu_topbar = Menu(name='topbar')
menu_user = Menu( menu_user = Menu(
icon_class=icon_menu_user, name='user', label=_('User') icon_class=icon_menu_user, name='user', label=_('User')
) )

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -8,6 +9,7 @@ from django.urls import reverse
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import ungettext, ugettext_lazy as _
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.permissions import Permission from mayan.apps.permissions import Permission
@@ -17,6 +19,28 @@ from .literals import PK_LIST_SEPARATOR
from .settings import setting_home_view from .settings import setting_home_view
class ContentTypeViewMixin(object):
"""
This mixin makes it easier for views to retrieve a content type from
the URL pattern.
"""
content_type_url_kw_args = {
'app_label': 'app_label',
'model_name': 'model'
}
def get_content_type(self):
return get_object_or_404(
klass=ContentType,
app_label=self.kwargs[
self.content_type_url_kw_args['app_label']
],
model=self.kwargs[
self.content_type_url_kw_args['model_name']
]
)
class DeleteExtraDataMixin(object): class DeleteExtraDataMixin(object):
""" """
Mixin to populate the extra data needed for delete views Mixin to populate the extra data needed for delete views
@@ -103,7 +127,15 @@ class ExternalObjectMixin(object):
'get_external_object_queryset() method.' 'get_external_object_queryset() method.'
) )
return self.external_object_queryset or self.external_object_class.objects.all() queryset = self.external_object_queryset
if not queryset:
manager = ModelPermission.get_manager(
model=self.external_object_class
)
queryset = manager.all()
return queryset
def get_external_object_queryset_filtered(self): def get_external_object_queryset_filtered(self):
queryset = self.get_external_object_queryset() queryset = self.get_external_object_queryset()
@@ -118,6 +150,20 @@ class ExternalObjectMixin(object):
return queryset return queryset
class ExternalContentTypeObjectMixin(ContentTypeViewMixin, ExternalObjectMixin):
"""
Mixin to retrieve an external object by content type from the URL pattern.
"""
external_object_pk_url_kwarg = 'object_id'
def get_external_object_queryset(self):
content_type = self.get_content_type()
self.external_object_class = content_type.model_class()
return super(
ExternalContentTypeObjectMixin, self
).get_external_object_queryset()
class FormExtraKwargsMixin(object): class FormExtraKwargsMixin(object):
""" """
Mixin that allows a view to pass extra keyword arguments to forms Mixin that allows a view to pass extra keyword arguments to forms
@@ -250,9 +296,9 @@ class ObjectActionMixin(object):
def get_success_message(self, count): def get_success_message(self, count):
return ungettext( return ungettext(
self.success_message, singular=self.success_message,
self.success_message_plural, plural=self.success_message_plural,
count number=count
) % { ) % {
'count': count, 'count': count,
} }
@@ -271,14 +317,15 @@ class ObjectActionMixin(object):
pass pass
except ActionError: except ActionError:
messages.error( messages.error(
self.request, self.error_message % {'instance': instance} message=self.error_message % {'instance': instance},
request=self.request
) )
else: else:
self.action_count += 1 self.action_count += 1
messages.success( messages.success(
self.request, message=self.get_success_message(count=self.action_count),
self.get_success_message(count=self.action_count) request=self.request
) )

View File

@@ -0,0 +1,22 @@
from __future__ import unicode_literals
import yaml
try:
from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
except ImportError:
from yaml import SafeLoader, SafeDumper
def yaml_dump(*args, **kwargs):
defaults = {'Dumper': SafeDumper}
defaults.update(kwargs)
return yaml.dump(*args, **defaults)
def yaml_load(*args, **kwargs):
defaults = {'Loader': SafeLoader}
defaults.update(kwargs)
return yaml.load(*args, **defaults)

View File

@@ -10,7 +10,6 @@ from mayan.apps.smart_settings.classes import Namespace
from .literals import DEFAULT_COMMON_HOME_VIEW from .literals import DEFAULT_COMMON_HOME_VIEW
namespace = Namespace(label=_('Common'), name='common') namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting( setting_auto_logging = namespace.add_setting(
@@ -95,322 +94,5 @@ setting_shared_storage = namespace.add_setting(
) )
setting_shared_storage_arguments = namespace.add_setting( setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS', global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default='{{location: {}}}'.format( default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
os.path.join(settings.MEDIA_ROOT, 'shared_files')
), quoted=True
)
namespace = Namespace(label=_('Django'), name='django')
setting_django_allowed_hosts = namespace.add_setting(
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS,
help_text=_(
'A list of strings representing the host/domain names that this site '
'can serve. This is a security measure to prevent HTTP Host header '
'attacks, which are possible even under many seemingly-safe web '
'server configurations. Values in this list can be '
'fully qualified names (e.g. \'www.example.com\'), in which case '
'they will be matched against the request\'s Host header exactly '
'(case-insensitive, not including port). A value beginning with a '
'period can be used as a subdomain wildcard: \'.example.com\' will '
'match example.com, www.example.com, and any other subdomain of '
'example.com. A value of \'*\' will match anything; in this case you '
'are responsible to provide your own validation of the Host header '
'(perhaps in a middleware; if so this middleware must be listed '
'first in MIDDLEWARE).'
),
)
setting_django_append_slash = namespace.add_setting(
global_name='APPEND_SLASH', default=settings.APPEND_SLASH,
help_text=_(
'When set to True, if the request URL does not match any of the '
'patterns in the URLconf and it doesn\'t end in a slash, an HTTP '
'redirect is issued to the same URL with a slash appended. Note '
'that the redirect may cause any data submitted in a POST request '
'to be lost. The APPEND_SLASH setting is only used if '
'CommonMiddleware is installed (see Middleware). See also '
'PREPEND_WWW.'
)
)
setting_django_auth_password_validators = namespace.add_setting(
global_name='AUTH_PASSWORD_VALIDATORS',
default=settings.AUTH_PASSWORD_VALIDATORS,
help_text=_(
'The list of validators that are used to check the strength of '
'user\'s passwords.'
)
)
setting_django_databases = namespace.add_setting(
global_name='DATABASES', default=settings.DATABASES,
help_text=_(
'A dictionary containing the settings for all databases to be used '
'with Django. It is a nested dictionary whose contents map a '
'database alias to a dictionary containing the options for an '
'individual database. The DATABASES setting must configure a '
'default database; any number of additional databases may also '
'be specified.'
),
)
setting_django_data_upload_max_memory_size = namespace.add_setting(
global_name='DATA_UPLOAD_MAX_MEMORY_SIZE',
default=settings.DATA_UPLOAD_MAX_MEMORY_SIZE,
help_text=_(
'Default: 2621440 (i.e. 2.5 MB). The maximum size in bytes that a '
'request body may be before a SuspiciousOperation '
'(RequestDataTooBig) is raised. The check is done when accessing '
'request.body or request.POST and is calculated against the total '
'request size excluding any file upload data. You can set this to '
'None to disable the check. Applications that are expected to '
'receive unusually large form posts should tune this setting. The '
'amount of request data is correlated to the amount of memory '
'needed to process the request and populate the GET and POST '
'dictionaries. Large requests could be used as a '
'denial-of-service attack vector if left unchecked. Since web '
'servers don\'t typically perform deep request inspection, it\'s '
'not possible to perform a similar check at that level. See also '
'FILE_UPLOAD_MAX_MEMORY_SIZE.'
),
)
setting_django_default_from_email = namespace.add_setting(
global_name='DEFAULT_FROM_EMAIL',
default=settings.DEFAULT_FROM_EMAIL,
help_text=_(
'Default: \'webmaster@localhost\' '
'Default email address to use for various automated correspondence '
'from the site manager(s). This doesn\'t include error messages sent '
'to ADMINS and MANAGERS; for that, see SERVER_EMAIL.'
),
)
setting_django_disallowed_user_agents = namespace.add_setting(
global_name='DISALLOWED_USER_AGENTS',
default=settings.DISALLOWED_USER_AGENTS,
help_text=_(
'Default: [] (Empty list). List of compiled regular expression '
'objects representing User-Agent strings that are not allowed to '
'visit any page, systemwide. Use this for bad robots/crawlers. '
'This is only used if CommonMiddleware is installed '
'(see Middleware).'
),
)
setting_django_email_backend = namespace.add_setting(
global_name='EMAIL_BACKEND',
default=settings.EMAIL_BACKEND,
help_text=_(
'Default: \'django.core.mail.backends.smtp.EmailBackend\'. The '
'backend to use for sending emails.'
),
)
setting_django_email_host = namespace.add_setting(
global_name='EMAIL_HOST',
default=settings.EMAIL_HOST,
help_text=_(
'Default: \'localhost\'. The host to use for sending email.'
),
)
setting_django_email_host_password = namespace.add_setting(
global_name='EMAIL_HOST_PASSWORD',
default=settings.EMAIL_HOST_PASSWORD,
help_text=_(
'Default: \'\' (Empty string). Password to use for the SMTP '
'server defined in EMAIL_HOST. This setting is used in '
'conjunction with EMAIL_HOST_USER when authenticating to the '
'SMTP server. If either of these settings is empty, '
'Django won\'t attempt authentication.'
),
)
setting_django_email_host_user = namespace.add_setting(
global_name='EMAIL_HOST_USER',
default=settings.EMAIL_HOST_USER,
help_text=_(
'Default: \'\' (Empty string). Username to use for the SMTP '
'server defined in EMAIL_HOST. If empty, Django won\'t attempt '
'authentication.'
),
)
setting_django_email_port = namespace.add_setting(
global_name='EMAIL_PORT',
default=settings.EMAIL_PORT,
help_text=_(
'Default: 25. Port to use for the SMTP server defined in EMAIL_HOST.'
),
)
setting_django_email_timeout = namespace.add_setting(
global_name='EMAIL_TIMEOUT',
default=settings.EMAIL_TIMEOUT,
help_text=_(
'Default: None. Specifies a timeout in seconds for blocking '
'operations like the connection attempt.'
),
)
setting_django_email_user_tls = namespace.add_setting(
global_name='EMAIL_USE_TLS',
default=settings.EMAIL_USE_TLS,
help_text=_(
'Default: False. Whether to use a TLS (secure) connection when '
'talking to the SMTP server. This is used for explicit TLS '
'connections, generally on port 587. If you are experiencing '
'hanging connections, see the implicit TLS setting EMAIL_USE_SSL.'
),
)
setting_django_email_user_ssl = namespace.add_setting(
global_name='EMAIL_USE_SSL',
default=settings.EMAIL_USE_SSL,
help_text=_(
'Default: False. Whether to use an implicit TLS (secure) connection '
'when talking to the SMTP server. In most email documentation this '
'type of TLS connection is referred to as SSL. It is generally used '
'on port 465. If you are experiencing problems, see the explicit '
'TLS setting EMAIL_USE_TLS. Note that EMAIL_USE_TLS/EMAIL_USE_SSL '
'are mutually exclusive, so only set one of those settings to True.'
),
)
setting_django_file_upload_max_memory_size = namespace.add_setting(
global_name='FILE_UPLOAD_MAX_MEMORY_SIZE',
default=settings.FILE_UPLOAD_MAX_MEMORY_SIZE,
help_text=_(
'Default: 2621440 (i.e. 2.5 MB). The maximum size (in bytes) '
'that an upload will be before it gets streamed to the file '
'system. See Managing files for details. See also '
'DATA_UPLOAD_MAX_MEMORY_SIZE.'
),
)
setting_django_login_url = namespace.add_setting(
global_name='LOGIN_URL',
default=settings.LOGIN_URL,
help_text=_(
'Default: \'/accounts/login/\' The URL where requests are '
'redirected for login, especially when using the login_required() '
'decorator. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you '
'don\'t have to define the URL in two places (settings '
'and URLconf).'
)
)
setting_django_login_redirect_url = namespace.add_setting(
global_name='LOGIN_REDIRECT_URL',
default=settings.LOGIN_REDIRECT_URL,
help_text=_(
'Default: \'/accounts/profile/\' The URL where requests are '
'redirected after login when the contrib.auth.login view gets no '
'next parameter. This is used by the login_required() decorator, '
'for example. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you don\'t '
'have to define the URL in two places (settings and URLconf).'
),
)
setting_django_logout_redirect_url = namespace.add_setting(
global_name='LOGOUT_REDIRECT_URL',
default=settings.LOGOUT_REDIRECT_URL,
help_text=_(
'Default: None. The URL where requests are redirected after a user '
'logs out using LogoutView (if the view doesn\'t get a next_page '
'argument). If None, no redirect will be performed and the logout '
'view will be rendered. This setting also accepts named URL '
'patterns which can be used to reduce configuration duplication '
'since you don\'t have to define the URL in two places (settings '
'and URLconf).'
)
)
setting_django_internal_ips = namespace.add_setting(
global_name='INTERNAL_IPS',
default=settings.INTERNAL_IPS,
help_text=_(
'A list of IP addresses, as strings, that: Allow the debug() '
'context processor to add some variables to the template context. '
'Can use the admindocs bookmarklets even if not logged in as a '
'staff user. Are marked as "internal" (as opposed to "EXTERNAL") '
'in AdminEmailHandler emails.'
),
)
setting_django_languages = namespace.add_setting(
global_name='LANGUAGES',
default=settings.LANGUAGES,
help_text=_(
'A list of all available languages. The list is a list of '
'two-tuples in the format (language code, language name) '
'for example, (\'ja\', \'Japanese\'). This specifies which '
'languages are available for language selection. '
'Generally, the default value should suffice. Only set this '
'setting if you want to restrict language selection to a '
'subset of the Django-provided languages. '
),
)
setting_django_language_code = namespace.add_setting(
global_name='LANGUAGE_CODE',
default=settings.LANGUAGE_CODE,
help_text=_(
'A string representing the language code for this installation. '
'This should be in standard language ID format. For example, U.S. '
'English is "en-us". It serves two purposes: If the locale '
'middleware isn\'t in use, it decides which translation is served '
'to all users. If the locale middleware is active, it provides a '
'fallback language in case the user\'s preferred language can\'t '
'be determined or is not supported by the website. It also provides '
'the fallback translation when a translation for a given literal '
'doesn\'t exist for the user\'s preferred language.'
),
)
setting_django_static_url = namespace.add_setting(
global_name='STATIC_URL',
default=settings.STATIC_URL,
help_text=_(
'URL to use when referring to static files located in STATIC_ROOT. '
'Example: "/static/" or "http://static.example.com/" '
'If not None, this will be used as the base path for asset '
'definitions (the Media class) and the staticfiles app. '
'It must end in a slash if set to a non-empty value.'
),
)
setting_django_staticfiles_storage = namespace.add_setting(
global_name='STATICFILES_STORAGE',
default=settings.STATICFILES_STORAGE,
help_text=_(
'The file storage engine to use when collecting static files with '
'the collectstatic management command. A ready-to-use instance of '
'the storage backend defined in this setting can be found at '
'django.contrib.staticfiles.storage.staticfiles_storage.'
),
)
setting_django_time_zone = namespace.add_setting(
global_name='TIME_ZONE',
default=settings.TIME_ZONE,
help_text=_(
'A string representing the time zone for this installation. '
'Note that this isn\'t necessarily the time zone of the server. '
'For example, one server may serve multiple Django-powered sites, '
'each with a separate time zone setting.'
),
)
setting_django_wsgi_application = namespace.add_setting(
global_name='WSGI_APPLICATION',
default=settings.WSGI_APPLICATION,
help_text=_(
'The full Python path of the WSGI application object that Django\'s '
'built-in servers (e.g. runserver) will use. The django-admin '
'startproject management command will create a simple wsgi.py '
'file with an application callable in it, and point this setting '
'to that application.'
),
)
namespace = Namespace(label=_('Celery'), name='celery')
setting_celery_broker_url = namespace.add_setting(
global_name='BROKER_URL', default=settings.BROKER_URL,
help_text=_(
'Default: "amqp://". Default broker URL. This must be a URL in '
'the form of: transport://userid:password@hostname:port/virtual_host '
'Only the scheme part (transport://) is required, the rest is '
'optional, and defaults to the specific transports default values.'
),
)
setting_celery_result_backend = namespace.add_setting(
global_name='CELERY_RESULT_BACKEND',
default=settings.CELERY_RESULT_BACKEND,
help_text=_(
'Default: No result backend enabled by default. The backend used '
'to store task results (tombstones). Refer to '
'http://docs.celeryproject.org/en/v4.1.0/userguide/configuration.'
'html#result-backend'
)
) )

View File

@@ -1,23 +1,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import yaml from mayan.apps.storage.utils import get_storage_subclass
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.module_loading import import_string
from .settings import ( from .settings import (
setting_shared_storage, setting_shared_storage_arguments setting_shared_storage, setting_shared_storage_arguments
) )
storage_sharedupload = import_string( storage_sharedupload = get_storage_subclass(
dotted_path=setting_shared_storage.value dotted_path=setting_shared_storage.value
)( )(**setting_shared_storage_arguments.value)
**yaml.load(
stream=setting_shared_storage_arguments.value or '{}',
Loader=SafeLoader
)
)

View File

@@ -7,6 +7,7 @@ from django_downloadview import assert_download_response
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
from mayan.apps.permissions.classes import Permission from mayan.apps.permissions.classes import Permission
from mayan.apps.smart_settings.classes import Namespace from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.tests.mixins import UserTestMixin
from .mixins import ( from .mixins import (
ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin, ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin,
@@ -21,7 +22,7 @@ class BaseTestCase(
SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin, SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin,
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin,
ModelTestCaseMixin, OpenFileCheckTestCaseMixin, ModelTestCaseMixin, OpenFileCheckTestCaseMixin,
TempfileCheckTestCasekMixin, TestCase TempfileCheckTestCasekMixin, UserTestMixin, TestCase
): ):
""" """
This is the most basic test case class any test in the project should use. This is the most basic test case class any test in the project should use.

View File

@@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from mayan.apps.acls.classes import ModelPermission from mayan.apps.acls.classes import ModelPermission
from mayan.apps.user_management.tests.mixins import UserTestMixin
from ..models import ErrorLogEntry from ..models import ErrorLogEntry
from ..permissions_runtime import permission_error_log_view from ..permissions_runtime import permission_error_log_view
@@ -13,7 +12,7 @@ from .base import GenericViewTestCase
from .literals import TEST_ERROR_LOG_ENTRY_RESULT from .literals import TEST_ERROR_LOG_ENTRY_RESULT
class CommonViewTestCase(UserTestMixin, GenericViewTestCase): class CommonViewTestCase(GenericViewTestCase):
def _request_about_view(self): def _request_about_view(self):
return self.get(viewname='common:about_view') return self.get(viewname='common:about_view')

View File

@@ -1,6 +1,10 @@
from __future__ import absolute_import, unicode_literals
from contextlib import contextmanager from contextlib import contextmanager
import sys import sys
from django.utils.encoding import force_text
class NullFile(object): class NullFile(object):
def write(self, string): def write(self, string):
@@ -13,3 +17,9 @@ def mute_stdout():
sys.stdout = NullFile() sys.stdout = NullFile()
yield yield
sys.stdout = stdout_old sys.stdout = stdout_old
def as_id_list(items):
return ','.join(
[force_text(item.pk) for item in items]
)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from django.views.i18n import JavaScriptCatalog, set_language from django.views.i18n import JavaScriptCatalog
from .api_views import ( from .api_views import (
APIContentTypeList, APITemplateDetailView, APITemplateListView APIContentTypeList, APITemplateDetailView, APITemplateListView
@@ -10,30 +10,10 @@ from .views import (
AboutView, CurrentUserLocaleProfileDetailsView, AboutView, CurrentUserLocaleProfileDetailsView,
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView, CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
LicenseView, ObjectErrorLogEntryListClearView, ObjectErrorLogEntryListView, LicenseView, ObjectErrorLogEntryListClearView, ObjectErrorLogEntryListView,
RootView, SetupListView, ToolsListView, multi_object_action_view RootView, SetupListView, ToolsListView
) )
urlpatterns = [ urlpatterns_error_logs = [
url(regex=r'^$', view=RootView.as_view(), name='root'),
url(regex=r'^home/$', view=HomeView.as_view(), name='home'),
url(regex=r'^about/$', view=AboutView.as_view(), name='about_view'),
url(regex=r'^license/$', view=LicenseView.as_view(), name='license_view'),
url(
regex=r'^object/multiple/action/$', view=multi_object_action_view,
name='multi_object_action_view'
),
url(regex=r'^setup/$', view=SetupListView.as_view(), name='setup_list'),
url(regex=r'^tools/$', view=ToolsListView.as_view(), name='tools_list'),
url(
regex=r'^user/locale/$',
view=CurrentUserLocaleProfileDetailsView.as_view(),
name='current_user_locale_profile_details'
),
url(
regex=r'^user/locale/edit/$',
view=CurrentUserLocaleProfileEditView.as_view(),
name='current_user_locale_profile_edit'
),
url( url(
regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$', regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
view=ObjectErrorLogEntryListView.as_view(), name='object_error_list' view=ObjectErrorLogEntryListView.as_view(), name='object_error_list'
@@ -45,7 +25,20 @@ urlpatterns = [
), ),
] ]
urlpatterns += [ urlpatterns_user_locale = [
url(
regex=r'^user/locale/$',
view=CurrentUserLocaleProfileDetailsView.as_view(),
name='current_user_locale_profile_details'
),
url(
regex=r'^user/locale/edit/$',
view=CurrentUserLocaleProfileEditView.as_view(),
name='current_user_locale_profile_edit'
),
]
urlpatterns_misc = [
url( url(
regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view() regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
), ),
@@ -53,11 +46,21 @@ urlpatterns += [
regex=r'^jsi18n/(?P<packages>\S+?)/$', view=JavaScriptCatalog.as_view(), regex=r'^jsi18n/(?P<packages>\S+?)/$', view=JavaScriptCatalog.as_view(),
name='javascript_catalog' name='javascript_catalog'
), ),
url(
regex=r'^set_language/$', view=set_language, name='set_language'
),
] ]
urlpatterns = [
url(regex=r'^$', view=RootView.as_view(), name='root'),
url(regex=r'^home/$', view=HomeView.as_view(), name='home'),
url(regex=r'^about/$', view=AboutView.as_view(), name='about_view'),
url(regex=r'^license/$', view=LicenseView.as_view(), name='license_view'),
url(regex=r'^setup/$', view=SetupListView.as_view(), name='setup_list'),
url(regex=r'^tools/$', view=ToolsListView.as_view(), name='tools_list'),
]
urlpatterns.extend(urlpatterns_error_logs)
urlpatterns.extend(urlpatterns_misc)
urlpatterns.extend(urlpatterns_user_locale)
api_urls = [ api_urls = [
url( url(
regex=r'^content_types/$', view=APIContentTypeList.as_view(), regex=r'^content_types/$', view=APIContentTypeList.as_view(),

View File

@@ -21,14 +21,6 @@ def check_for_sqlite():
return settings.DATABASES['default']['ENGINE'] == DJANGO_SQLITE_BACKEND and settings.DEBUG is False return settings.DATABASES['default']['ENGINE'] == DJANGO_SQLITE_BACKEND and settings.DEBUG is False
def encapsulate(function):
# Workaround Django ticket 15791
# Changeset 16045
# http://stackoverflow.com/questions/6861601/
# cannot-resolve-callable-context-variable/6955045#6955045
return lambda: function
def get_related_field(model, related_field_name): def get_related_field(model, related_field_name):
try: try:
local_field_name, remaining_field_path = related_field_name.split( local_field_name, remaining_field_path = related_field_name.split(

View File

@@ -1,9 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json
import re import re
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.utils import six from django.utils import six
from django.utils.deconstruct import deconstructible
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -23,6 +32,54 @@ def _lazy_re_compile(regex, flags=0):
return SimpleLazyObject(_compile) return SimpleLazyObject(_compile)
@deconstructible
class JSONValidator(object):
"""
Validates that the input is JSON compliant.
"""
def __call__(self, value):
value = value.strip()
try:
json.loads(stream=value)
except ValueError:
raise ValidationError(
_('Enter a valid JSON value.'),
code='invalid'
)
def __eq__(self, other):
return (
isinstance(other, JSONValidator)
)
def __ne__(self, other):
return not (self == other)
@deconstructible
class YAMLValidator(object):
"""
Validates that the input is YAML compliant.
"""
def __call__(self, value):
value = value.strip()
try:
yaml.load(stream=value, Loader=SafeLoader)
except yaml.error.YAMLError:
raise ValidationError(
_('Enter a valid YAML value.'),
code='invalid'
)
def __eq__(self, other):
return (
isinstance(other, YAMLValidator)
)
def __ne__(self, other):
return not (self == other)
internal_name_re = _lazy_re_compile(r'^[a-zA-Z0-9_]+\Z') internal_name_re = _lazy_re_compile(r'^[a-zA-Z0-9_]+\Z')
validate_internal_name = RegexValidator( validate_internal_name = RegexValidator(
internal_name_re, _( internal_name_re, _(

View File

@@ -1,15 +1,11 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from json import dumps
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone, translation from django.utils import timezone, translation
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView from django.views.generic import RedirectView
@@ -21,7 +17,7 @@ from .forms import (
from .generics import ( from .generics import (
ConfirmView, SingleObjectEditView, SingleObjectListView, SimpleView ConfirmView, SingleObjectEditView, SingleObjectListView, SimpleView
) )
from .icons import icon_setup from .icons import icon_object_errors, icon_setup
from .menus import menu_tools, menu_setup from .menus import menu_tools, menu_setup
from .permissions_runtime import permission_error_log_view from .permissions_runtime import permission_error_log_view
from .settings import setting_home_view from .settings import setting_home_view
@@ -155,6 +151,14 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
{'name': _('Result'), 'attribute': 'result'}, {'name': _('Result'), 'attribute': 'result'},
), ),
'hide_object': True, 'hide_object': True,
'no_results_icon': icon_object_errors,
'no_results_text': _(
'This view displays the error log of different object. '
'An empty list is a good thing.'
),
'no_results_title': _(
'There are no error log entries'
),
'object': self.get_object(), 'object': self.get_object(),
'title': _('Error log entries for: %s' % self.get_object()), 'title': _('Error log entries for: %s' % self.get_object()),
} }
@@ -212,67 +216,3 @@ class ToolsListView(SimpleView):
'These modules are used to do system maintenance.' 'These modules are used to do system maintenance.'
) )
} }
def multi_object_action_view(request):
"""
Proxy view called first when using a multi object action, which
then redirects to the appropriate specialized view
"""
next = request.POST.get(
'next', request.GET.get(
'next', request.META.get(
'HTTP_REFERER', reverse(setting_home_view.value)
)
)
)
action = request.GET.get('action', None)
id_list = ','.join(
[key[3:] for key in request.GET.keys() if key.startswith('pk_')]
)
items_property_list = [
(key[11:]) for key in request.GET.keys() if key.startswith('properties_')
]
if not action:
messages.error(
message=_('No action selected.'), request=request
)
return HttpResponseRedirect(
redirect_to=request.META.get(
'HTTP_REFERER', reverse(setting_home_view.value)
)
)
if not id_list and not items_property_list:
messages.error(
message=_('Must select at least one item.'),
request=request
)
return HttpResponseRedirect(
redirect_to=request.META.get(
'HTTP_REFERER', reverse(setting_home_view.value)
)
)
# Separate redirects to keep backwards compatibility with older
# functions that don't expect a properties_list parameter
if items_property_list:
return HttpResponseRedirect(
redirect_to='%s?%s' % (
action,
urlencode(
{
'items_property_list': dumps(items_property_list),
'next': next
}
)
)
)
else:
return HttpResponseRedirect(
redirect_to='%s?%s' % (
action, urlencode({'id_list': id_list, 'next': next})
)
)

View File

@@ -7,11 +7,6 @@ import shutil
from PIL import Image from PIL import Image
import PyPDF2 import PyPDF2
import sh import sh
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -20,16 +15,14 @@ from mayan.apps.storage.utils import NamedTemporaryFile
from ..classes import ConverterBase from ..classes import ConverterBase
from ..exceptions import PageCountError from ..exceptions import PageCountError
from ..settings import setting_graphics_backend_config from ..settings import setting_graphics_backend_arguments
from ..literals import ( from ..literals import (
DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFTOPPM_DPI, DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH,
DEFAULT_PDFINFO_PATH DEFAULT_PDFINFO_PATH
) )
pdftoppm_path = yaml.load( pdftoppm_path = setting_graphics_backend_arguments.value.get(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdftoppm_path', DEFAULT_PDFTOPPM_PATH 'pdftoppm_path', DEFAULT_PDFTOPPM_PATH
) )
@@ -39,26 +32,20 @@ except sh.CommandNotFound:
pdftoppm = None pdftoppm = None
else: else:
pdftoppm_format = '-{}'.format( pdftoppm_format = '-{}'.format(
yaml.load( setting_graphics_backend_arguments.value.get(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT 'pdftoppm_format', DEFAULT_PDFTOPPM_FORMAT
) )
) )
pdftoppm_dpi = format( pdftoppm_dpi = format(
yaml.load( setting_graphics_backend_arguments.value.get(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI 'pdftoppm_dpi', DEFAULT_PDFTOPPM_DPI
) )
) )
pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi) pdftoppm = pdftoppm.bake(pdftoppm_format, '-r', pdftoppm_dpi)
pdfinfo_path = yaml.load( pdfinfo_path = setting_graphics_backend_arguments.value.get(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pdfinfo_path', DEFAULT_PDFINFO_PATH 'pdfinfo_path', DEFAULT_PDFINFO_PATH
) )

View File

@@ -7,12 +7,6 @@ import shutil
from PIL import Image from PIL import Image
import sh import sh
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -27,16 +21,14 @@ from .literals import (
CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH, CONVERTER_OFFICE_FILE_MIMETYPES, DEFAULT_LIBREOFFICE_PATH,
DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT DEFAULT_PAGE_NUMBER, DEFAULT_PILLOW_FORMAT
) )
from .settings import setting_graphics_backend_config from .settings import setting_graphics_backend_arguments
logger = logging.getLogger(__name__) libreoffice_path = setting_graphics_backend_arguments.value.get(
BACKEND_CONFIG = yaml.load(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
)
libreoffice_path = BACKEND_CONFIG.get(
'libreoffice_path', DEFAULT_LIBREOFFICE_PATH 'libreoffice_path', DEFAULT_LIBREOFFICE_PATH
) )
logger = logging.getLogger(__name__)
class ConverterBase(object): class ConverterBase(object):
def __init__(self, file_object, mime_type=None): def __init__(self, file_object, mime_type=None):
@@ -62,9 +54,7 @@ class ConverterBase(object):
pass pass
def get_page(self, output_format=None): def get_page(self, output_format=None):
output_format = output_format or yaml.load( output_format = output_format or setting_graphics_backend_arguments.value.get(
stream=setting_graphics_backend_config.value, Loader=SafeLoader
).get(
'pillow_format', DEFAULT_PILLOW_FORMAT 'pillow_format', DEFAULT_PILLOW_FORMAT
) )
@@ -156,7 +146,7 @@ class ConverterBase(object):
logger.error('Exception launching Libre Office; %s', exception) logger.error('Exception launching Libre Office; %s', exception)
raise raise
finally: finally:
fs_cleanup(libreoffice_home_directory) fs_cleanup(filename=libreoffice_home_directory)
# LibreOffice return a PDF file with the same name as the input # LibreOffice return a PDF file with the same name as the input
# provided but with the .pdf extension. # provided but with the .pdf extension.
@@ -190,7 +180,7 @@ class ConverterBase(object):
shutil.copyfileobj( shutil.copyfileobj(
fsrc=converted_file_object, fdst=temporary_converted_file_object fsrc=converted_file_object, fdst=temporary_converted_file_object
) )
fs_cleanup(converted_file_path) fs_cleanup(filename=converted_file_path)
temporary_converted_file_object.seek(0) temporary_converted_file_object.seek(0)
return temporary_converted_file_object return temporary_converted_file_object

View File

@@ -2,15 +2,12 @@ from __future__ import unicode_literals
import yaml import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
from .models import Transformation from .models import Transformation
@@ -21,7 +18,7 @@ class TransformationForm(forms.ModelForm):
def clean(self): def clean(self):
try: try:
yaml.load(stream=self.cleaned_data['arguments'], Loader=SafeLoader) yaml_load(stream=self.cleaned_data['arguments'])
except yaml.YAMLError: except yaml.YAMLError:
raise ValidationError( raise ValidationError(
_( _(

View File

@@ -2,16 +2,11 @@ from __future__ import unicode_literals
import logging import logging
import yaml
try:
from yaml import CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
except ImportError:
from yaml import SafeLoader, SafeDumper
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models, transaction from django.db import models, transaction
from mayan.apps.common.serialization import yaml_dump, yaml_load
from .transformations import BaseTransformation from .transformations import BaseTransformation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,8 +18,8 @@ class TransformationManager(models.Manager):
self.create( self.create(
content_type=content_type, object_id=obj.pk, content_type=content_type, object_id=obj.pk,
name=transformation.name, arguments=yaml.dump( name=transformation.name, arguments=yaml_dump(
data=arguments, Dumper=SafeDumper data=arguments
) )
) )
@@ -96,9 +91,8 @@ class TransformationManager(models.Manager):
# Some transformations don't require arguments # Some transformations don't require arguments
# return an empty dictionary as ** doesn't allow None # return an empty dictionary as ** doesn't allow None
if transformation.arguments: if transformation.arguments:
kwargs = yaml.load( kwargs = yaml_load(
stream=transformation.arguments, stream=transformation.arguments,
Loader=SafeLoader
) )
else: else:
kwargs = {} kwargs = {}

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-06-26 19:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('converter', '0013_auto_20180823_2353'),
]
operations = [
migrations.AlterField(
model_name='transformation',
name='name',
field=models.CharField(choices=[('crop', 'Crop: left, top, right, bottom'), ('draw_rectangle', 'Draw rectangle: left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('draw_rectangle_percent', 'Draw rectangle (percents coordinates): left, top, right, bottom, fillcolor, outlinecolor, outlinewidth'), ('flip', 'Flip'), ('gaussianblur', 'Gaussian blur: radius'), ('lineart', 'Line art'), ('mirror', 'Mirror'), ('resize', 'Resize: width, height'), ('rotate', 'Rotate: degrees, fillcolor'), ('rotate180', 'Rotate 180 degrees'), ('rotate270', 'Rotate 270 degrees'), ('rotate90', 'Rotate 90 degrees'), ('unsharpmask', 'Unsharp masking: radius, percent, threshold'), ('zoom', 'Zoom: percent')], max_length=128, verbose_name='Name'),
),
]

View File

@@ -16,22 +16,15 @@ setting_graphics_backend = namespace.add_setting(
help_text=_('Graphics conversion backend to use.'), help_text=_('Graphics conversion backend to use.'),
global_name='CONVERTER_GRAPHICS_BACKEND', global_name='CONVERTER_GRAPHICS_BACKEND',
) )
setting_graphics_backend_config = namespace.add_setting( setting_graphics_backend_arguments = namespace.add_setting(
default=''' default={
{{ 'libreoffice_path': DEFAULT_LIBREOFFICE_PATH,
libreoffice_path: {}, 'pdftoppm_dpi': DEFAULT_PDFTOPPM_DPI,
pdftoppm_dpi: {}, 'pdftoppm_format': DEFAULT_PDFTOPPM_FORMAT,
pdftoppm_format: {}, 'pdftoppm_path': DEFAULT_PDFTOPPM_PATH,
pdftoppm_path: {}, 'pdfinfo_path': DEFAULT_PDFINFO_PATH,
pdfinfo_path: {}, 'pillow_format': DEFAULT_PILLOW_FORMAT,
pillow_format: {} }, help_text=_(
}}
'''.replace('\n', '').format(
DEFAULT_LIBREOFFICE_PATH, DEFAULT_PDFTOPPM_DPI,
DEFAULT_PDFTOPPM_FORMAT, DEFAULT_PDFTOPPM_PATH, DEFAULT_PDFINFO_PATH,
DEFAULT_PILLOW_FORMAT
), help_text=_(
'Configuration options for the graphics conversion backend.' 'Configuration options for the graphics conversion backend.'
), global_name='CONVERTER_GRAPHICS_BACKEND_CONFIG', quoted=True ), global_name='CONVERTER_GRAPHICS_BACKEND_ARGUMENTS'
) )

View File

@@ -121,7 +121,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={'top': '10'} arguments={'top': '10'}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())
def test_crop_transformation_invalid_arguments(self): def test_crop_transformation_invalid_arguments(self):
self._silence_logger(name='mayan.apps.converter.managers') self._silence_logger(name='mayan.apps.converter.managers')
@@ -132,8 +132,7 @@ class TransformationTestCase(GenericDocumentTestCase):
obj=document_page, transformation=TransformationCrop, obj=document_page, transformation=TransformationCrop,
arguments={'top': 'x', 'left': '-'} arguments={'top': 'x', 'left': '-'}
) )
self.assertTrue(document_page.generate_image())
self.assertTrue(document_page.generate_image().startswith('page'))
def test_crop_transformation_non_valid_range_arguments(self): def test_crop_transformation_non_valid_range_arguments(self):
self._silence_logger(name='mayan.apps.converter.managers') self._silence_logger(name='mayan.apps.converter.managers')
@@ -145,7 +144,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={'top': '-1000', 'bottom': '100000000'} arguments={'top': '-1000', 'bottom': '100000000'}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())
def test_crop_transformation_overlapping_ranges_arguments(self): def test_crop_transformation_overlapping_ranges_arguments(self):
self._silence_logger(name='mayan.apps.converter.managers') self._silence_logger(name='mayan.apps.converter.managers')
@@ -162,7 +161,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={'left': '1000', 'right': '10000'} arguments={'left': '1000', 'right': '10000'}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())
def test_lineart_transformations(self): def test_lineart_transformations(self):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
@@ -172,7 +171,7 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={} arguments={}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())
def test_rotate_transformations(self): def test_rotate_transformations(self):
document_page = self.test_document.pages.first() document_page = self.test_document.pages.first()
@@ -182,18 +181,18 @@ class TransformationTestCase(GenericDocumentTestCase):
arguments={} arguments={}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())
Transformation.objects.add_to_object( Transformation.objects.add_to_object(
obj=document_page, transformation=TransformationRotate180, obj=document_page, transformation=TransformationRotate180,
arguments={} arguments={}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())
Transformation.objects.add_to_object( Transformation.objects.add_to_object(
obj=document_page, transformation=TransformationRotate270, obj=document_page, transformation=TransformationRotate270,
arguments={} arguments={}
) )
self.assertTrue(document_page.generate_image().startswith('page')) self.assertTrue(document_page.generate_image())

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import hashlib import hashlib
import logging import logging
from PIL import Image, ImageColor, ImageFilter from PIL import Image, ImageColor, ImageDraw, ImageFilter
from django.utils.translation import string_concat, ugettext_lazy as _ from django.utils.translation import string_concat, ugettext_lazy as _
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@@ -151,6 +151,208 @@ class TransformationCrop(BaseTransformation):
return self.image.crop((left, top, right, bottom)) return self.image.crop((left, top, right, bottom))
class TransformationDrawRectangle(BaseTransformation):
arguments = (
'left', 'top', 'right', 'bottom', 'fillcolor', 'outlinecolor',
'outlinewidth'
)
label = _('Draw rectangle')
name = 'draw_rectangle'
def execute_on(self, *args, **kwargs):
super(TransformationDrawRectangle, self).execute_on(*args, **kwargs)
try:
left = int(self.left or '0')
except ValueError:
left = 0
try:
top = int(self.top or '0')
except ValueError:
top = 0
try:
right = int(self.right or '0')
except ValueError:
right = 0
try:
bottom = int(self.bottom or '0')
except ValueError:
bottom = 0
if left < 0:
left = 0
if left > self.image.size[0] - 1:
left = self.image.size[0] - 1
if top < 0:
top = 0
if top > self.image.size[1] - 1:
top = self.image.size[1] - 1
if right < 0:
right = 0
if right > self.image.size[0] - 1:
right = self.image.size[0] - 1
if bottom < 0:
bottom = 0
if bottom > self.image.size[1] - 1:
bottom = self.image.size[1] - 1
# Invert right value
# Pillow uses left, top, right, bottom to define a viewport
# of real coordinates
# We invert the right and bottom to define a viewport
# that can crop from the right and bottom borders without
# having to know the real dimensions of an image
right = self.image.size[0] - right
bottom = self.image.size[1] - bottom
if left > right:
left = right - 1
if top > bottom:
top = bottom - 1
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
)
fillcolor_value = getattr(self, 'fillcolor', None)
if fillcolor_value:
fill_color = ImageColor.getrgb(fillcolor_value)
else:
fill_color = 0
outlinecolor_value = getattr(self, 'outlinecolor', None)
if outlinecolor_value:
outline_color = ImageColor.getrgb(outlinecolor_value)
else:
outline_color = None
outlinewidth_value = getattr(self, 'outlinewidth', None)
if outlinewidth_value:
outline_width = int(outlinewidth_value)
else:
outline_width = 0
draw = ImageDraw.Draw(self.image)
draw.rectangle(
(left, top, right, bottom), fill=fill_color, outline=outline_color,
width=outline_width
)
return self.image
class TransformationDrawRectanglePercent(BaseTransformation):
arguments = (
'left', 'top', 'right', 'bottom', 'fillcolor', 'outlinecolor',
'outlinewidth'
)
label = _('Draw rectangle (percents coordinates)')
name = 'draw_rectangle_percent'
def execute_on(self, *args, **kwargs):
super(TransformationDrawRectanglePercent, self).execute_on(*args, **kwargs)
try:
left = float(self.left or '0')
except ValueError:
left = 0
try:
top = float(self.top or '0')
except ValueError:
top = 0
try:
right = float(self.right or '0')
except ValueError:
right = 0
try:
bottom = float(self.bottom or '0')
except ValueError:
bottom = 0
if left < 0:
left = 0
if left > 100:
left = 100
if top < 0:
top = 0
if top > 100:
top = 100
if right < 0:
right = 0
if right > 100:
right = 100
if bottom < 0:
bottom = 0
if bottom > 100:
bottom = 100
logger.debug(
'left: %f, top: %f, right: %f, bottom: %f', left, top, right,
bottom
)
fillcolor_value = getattr(self, 'fillcolor', None)
if fillcolor_value:
fill_color = ImageColor.getrgb(fillcolor_value)
else:
fill_color = 0
outlinecolor_value = getattr(self, 'outlinecolor', None)
if outlinecolor_value:
outline_color = ImageColor.getrgb(outlinecolor_value)
else:
outline_color = None
outlinewidth_value = getattr(self, 'outlinewidth', None)
if outlinewidth_value:
outline_width = int(outlinewidth_value)
else:
outline_width = 0
left = left / 100.0 * self.image.size[0]
top = top / 100.0 * self.image.size[1]
# Invert right value
# Pillow uses left, top, right, bottom to define a viewport
# of real coordinates
# We invert the right and bottom to define a viewport
# that can crop from the right and bottom borders without
# having to know the real dimensions of an image
right = self.image.size[0] - (right / 100.0 * self.image.size[0])
bottom = self.image.size[1] - (bottom / 100.0 * self.image.size[1])
draw = ImageDraw.Draw(self.image)
draw.rectangle(
(left, top, right, bottom), fill=fill_color, outline=outline_color,
width=outline_width
)
return self.image
class TransformationFlip(BaseTransformation): class TransformationFlip(BaseTransformation):
arguments = () arguments = ()
label = _('Flip') label = _('Flip')
@@ -316,6 +518,10 @@ class TransformationZoom(BaseTransformation):
BaseTransformation.register(transformation=TransformationCrop) BaseTransformation.register(transformation=TransformationCrop)
BaseTransformation.register(transformation=TransformationDrawRectangle)
BaseTransformation.register(
transformation=TransformationDrawRectanglePercent
)
BaseTransformation.register(transformation=TransformationFlip) BaseTransformation.register(transformation=TransformationFlip)
BaseTransformation.register(transformation=TransformationGaussianBlur) BaseTransformation.register(transformation=TransformationGaussianBlur)
BaseTransformation.register(transformation=TransformationLineArt) BaseTransformation.register(transformation=TransformationLineArt)

View File

@@ -9,19 +9,19 @@ from .views import (
urlpatterns = [ urlpatterns = [
url( url(
regex=r'^create_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$', regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/$',
view=TransformationCreateView.as_view(), name='transformation_create'
),
url(
regex=r'^list_for/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/$',
view=TransformationListView.as_view(), name='transformation_list' view=TransformationListView.as_view(), name='transformation_list'
), ),
url( url(
regex=r'^delete/(?P<pk>\d+)/$', view=TransformationDeleteView.as_view(), regex=r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/transformations/create/$',
view=TransformationCreateView.as_view(), name='transformation_create'
),
url(
regex=r'^transformations/(?P<pk>\d+)/delete/$', view=TransformationDeleteView.as_view(),
name='transformation_delete' name='transformation_delete'
), ),
url( url(
regex=r'^edit/(?P<pk>\d+)/$', view=TransformationEditView.as_view(), regex=r'^transformations/(?P<pk>\d+)/edit/$', view=TransformationEditView.as_view(),
name='transformation_edit' name='transformation_edit'
), ),
] ]

View File

@@ -2,15 +2,12 @@ from __future__ import unicode_literals
import yaml import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.serialization import yaml_load
@deconstructible @deconstructible
class YAMLValidator(object): class YAMLValidator(object):
@@ -20,7 +17,7 @@ class YAMLValidator(object):
def __call__(self, value): def __call__(self, value):
value = value.strip() value = value.strip()
try: try:
yaml.load(stream=value, Loader=SafeLoader) yaml_load(stream=value)
except yaml.error.YAMLError: except yaml.error.YAMLError:
raise ValidationError( raise ValidationError(
_('Enter a valid YAML value.'), _('Enter a valid YAML value.'),

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