Compare commits

...

168 Commits

Author SHA1 Message Date
Roberto Rosario
0e35cca704 Add cabinets and metadata control codes
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-06 03:25:56 -04:00
Roberto Rosario
661301f057 Finalize icon
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-06 02:44:13 -04:00
Roberto Rosario
9448b148e9 Improve tests, update migrations
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-05 22:18:59 -04:00
Roberto Rosario
a13f033104 Add first control code
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-05 01:40:08 -04:00
Roberto Rosario
968abe2cdb Finish view tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-05 01:24:54 -04:00
Roberto Rosario
cca08d8103 Add initial tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-04 16:23:12 -04:00
HGWells
3872db8c9f Add code selecton and create views
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-04 07:41:11 -04:00
Roberto Rosario
1315a74e27 Add control sheet codes views
Add list, edit, delete views.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-03 16:15:08 -04:00
Roberto Rosario
8c6812203c Add permissions, setup views, icons
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-02 01:20:03 -04:00
Roberto Rosario
4ed36e0114 Add contol sheet preview
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-01 17:55:18 -04:00
Roberto Rosario
c48fc203e3 Add dependencies and cache handling
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-01 15:34:59 -04:00
Roberto Rosario
af71ba927f Put file cache label column first
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-01 15:34:31 -04:00
Roberto Rosario
894e009c2a Update file cache model
Index the name field. Add help texts for maximum size and current
size methods.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-01 15:33:41 -04:00
Roberto Rosario
f9ba08ee59 Add control code preview generation
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-09-01 14:35:04 -04:00
Roberto Rosario
2819f9445b Add control codes app proof of concept
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-30 05:22:13 -04:00
Roberto Rosario
1b2ed08c7c Enable web links app tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-30 02:36:54 -04:00
Roberto Rosario
c6c605e320 Add missing line
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-30 02:34:01 -04:00
Roberto Rosario
4b3ab82ee2 Fix autoadmin tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-29 23:51:57 -04:00
Roberto Rosario
f8eda67bd5 Add support for changing system messages position
GitLab issue #640. Thanks to Matthias Urhahn (@d4rken).

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-29 23:41:22 -04:00
Roberto Rosario
58bcf20a46 Remove tests * imports
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-29 23:10:28 -04:00
Roberto Rosario
49979dede5 Merge remote-tracking branch 'origin/versions/micro' into feature/merge_micro
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-29 00:07:18 -04:00
09f481f5f0 Unify all line endings to be Linux style.
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-28 23:51:59 -04:00
Roberto Rosario
a250919acc Merge remote-tracking branch 'origin/versions/micro' into features/micro_merge
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-25 23:48:12 -04:00
Roberto Rosario
38980e5f75 Rename test method names for clarity
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-25 22:06:34 -04:00
Roberto Rosario
6503d9474d Fix remaining tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-20 16:42:31 -04:00
Roberto Rosario
e7734def58 Fix documents app failing tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-20 05:03:24 -04:00
Roberto Rosario
f50d22b382 Update changelog
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-20 00:22:28 -04:00
Roberto Rosario
ad37228466 Add converter layers, redactions app
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-20 00:21:03 -04:00
Roberto Rosario
0917bd57b3 Add ACL filter support for case 6
Support inherited field of a related field that is Generic
Foreign Key.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-08-20 00:09:56 -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
500 changed files with 18709 additions and 7280 deletions

View File

@@ -115,6 +115,12 @@ source_lang = en
source_file = mayan/apps/events/locale/en/LC_MESSAGES/django.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]
file_filter = mayan/apps/file_metadata/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
@@ -222,3 +228,10 @@ file_filter = mayan/apps/user_management/locale/<lang>/LC_MESSAGES/django.po
source_lang = en
source_file = mayan/apps/user_management/locale/en/LC_MESSAGES/django.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

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 '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing
@@ -234,10 +234,10 @@ generate-requirements: ## Generate all requirements files from the project deped
# Dev 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.
./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.
./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
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 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',
'django_gpg', 'document_comments', 'document_indexing',
'document_parsing', 'document_signatures', 'document_states',
'documents', 'dynamic_search', 'events', 'file_metadata', 'linking',
'lock_manager', 'mayan_statistics', 'mailer', 'metadata', 'mirroring',
'motd', 'navigation', 'ocr', 'permissions', 'platform', 'rest_api',
'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
'user_management'
'documents', 'dynamic_search', 'events', 'file_caching',
'file_metadata', 'linking', 'lock_manager', 'mailer',
'mayan_statistics', 'metadata', 'mirroring', 'motd', 'navigation',
'ocr', 'permissions', 'platform', 'rest_api', 'smart_settings',
'sources', 'storage', 'tags', 'task_manager', 'user_management',
'weblinks'
)
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
####
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"
@@ -22,6 +22,7 @@ RUN set -x \
&& DEBIAN_FRONTEND=noninteractive \
apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
exiftool \
ghostscript \
gpgv \
@@ -29,11 +30,11 @@ apt-get update \
graphviz \
libfuse2 \
libmagic1 \
libmariadbclient18 \
libmariadb3 \
libreoffice \
libpq5 \
poppler-utils \
redis-server \
python3-distutils \
sane-utils \
sudo \
supervisor \
@@ -52,22 +53,20 @@ apt-get update \
&& if [ "$(uname -m)" = "armv7l" ]; then \
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ \
&& ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
; 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
; fi
####
# 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
FROM BASE_IMAGE as BUILDER_IMAGE
# Python libraries caching
ARG PIP_INDEX_URL
ARG PIP_TRUSTED_HOST
WORKDIR /src
# Copy the source files needed to build the Python package
@@ -96,39 +95,40 @@ apt-get install -y --no-install-recommends \
libssl-dev \
g++ \
gcc \
python-dev \
python-virtualenv \
python3-dev \
python3-venv \
&& mkdir -p "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan "${PROJECT_INSTALL_DIR}" \
&& chown -R mayan:mayan /src
USER mayan
RUN python -m virtualenv "${PROJECT_INSTALL_DIR}" \
RUN python3 -m venv "${PROJECT_INSTALL_DIR}" \
&& . "${PROJECT_INSTALL_DIR}/bin/activate" \
&& pip install --no-cache-dir --no-use-pep517 \
librabbitmq==1.6.1 \
mysql-python==1.2.5 \
psycopg2==2.7.3.2 \
redis==2.10.6 \
&& pip install --no-cache-dir \
librabbitmq==2.0.0 \
mysqlclient==1.4.2.post1 \
psycopg2==2.8.3 \
redis==3.2.1 \
flower==0.9.3 \
# psutil is needed by ARM builds otherwise gevent and gunicorn fail to start
&& 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 \
; fi \
# 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
&& python setup.py sdist \
&& python3 setup.py sdist \
# 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
&& mayan-edms.py installjavascript \
&& mayan-edms.py installdependencies \
&& MAYAN_STATIC_ROOT=${PROJECT_INSTALL_DIR}/static mayan-edms.py preparestatic --link --noinput
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
@@ -144,7 +144,7 @@ VOLUME ["/var/lib/mayan"]
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 8000
CMD ["mayan"]
CMD ["run_all"]
RUN ${PROJECT_INSTALL_DIR}/bin/mayan-edms.py platformtemplate supervisord_docker > /etc/supervisor/conf.d/mayan.conf \
&& 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`
CONSOLE_COLUMNS ?= `echo $$(tput cols)`
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-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 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: docker-build-with-proxy
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:
broker:
driver: local
app:
driver: local
db:
driver: local
results:
driver: local
networks:
mayan-bridge:
driver: bridge
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/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
app:
build:
context: ..
dockerfile: ./docker/Dockerfile
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
- postgresql
- redis
# Enable to use RabbitMQ
#- rabbitmq
environment: &mayan_env
# Enable to use RabbitMQ
# MAYAN_CELERY_BROKER_URL: amqp://mayan:mayanrabbitpass@broker:5672/mayan
# Disable Redis Broker to use RabbitMQ as Broker
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':'postgresql'}}"
image: mayanedms/mayanedms:3.2.6
networks:
- mayan-bridge
ports:
- "80:8000"
restart: unless-stopped
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
echo "mayan: starting entrypoint.sh"
@@ -11,17 +14,13 @@ DEFAULT_USER_GID=1000
MAYAN_USER_UID=${MAYAN_USER_UID:-${DEFAULT_USER_UID}}
MAYAN_USER_GID=${MAYAN_USER_GID:-${DEFAULT_USER_GID}}
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_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_PYTHON_BIN_DIR=/opt/mayan-edms/bin/
export MAYAN_MEDIA_ROOT=/var/lib/mayan
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_WORKERS=${MAYAN_GUNICORN_WORKERS:-2}
@@ -29,9 +28,9 @@ export MAYAN_GUNICORN_TIMEOUT=${MAYAN_GUNICORN_TIMEOUT:-120}
export MAYAN_PIP_BIN=${MAYAN_PYTHON_BIN_DIR}pip
export MAYAN_STATIC_ROOT=${MAYAN_INSTALL_DIR}/static
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-1}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-1}
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-1}
MAYAN_WORKER_FAST_CONCURRENCY=${MAYAN_WORKER_FAST_CONCURRENCY:-0}
MAYAN_WORKER_MEDIUM_CONCURRENCY=${MAYAN_WORKER_MEDIUM_CONCURRENCY:-0}
MAYAN_WORKER_SLOW_CONCURRENCY=${MAYAN_WORKER_SLOW_CONCURRENCY:-0}
update_uid_gid() {
echo "mayan: update_uid_gid()"
@@ -67,11 +66,9 @@ else
fi
export MAYAN_WORKER_SLOW_CONCURRENCY
export CELERY_ALWAYS_EAGER=False
# Allow importing of user setting modules
export PYTHONPATH=$PYTHONPATH:$MAYAN_MEDIA_ROOT
chown mayan:mayan /var/lib/mayan -R
apt_get_install() {
apt-get -q update
apt-get install -y --force-yes --no-install-recommends --auto-remove "$@"
@@ -79,9 +76,9 @@ apt_get_install() {
rm -rf /var/lib/apt/lists/*
}
initialize() {
echo "mayan: initialize()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-javascript"
initialsetup() {
echo "mayan: initialsetup()"
su mayan -c "${MAYAN_BIN} initialsetup --force --no-dependencies"
}
os_package_installs() {
@@ -98,43 +95,71 @@ pip_installs() {
fi
}
start() {
run_all() {
echo "mayan: start()"
rm -rf /var/run/supervisor.sock
exec /usr/bin/supervisord -nc /etc/supervisor/supervisord.conf
}
upgrade() {
echo "mayan: upgrade()"
su mayan -c "${MAYAN_BIN} performupgrade --no-javascript"
performupgrade() {
echo "mayan: performupgrade()"
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
pip_installs || true
chown mayan:mayan /var/lib/mayan -R
case "$1" in
mayan) # Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialize
else
upgrade
fi
start
run_initialsetup)
initialsetup
;;
run-tests) # Check if this is a new install, otherwise try to upgrade the existing
# installation on subsequent starts
if [ ! -f $INSTALL_FLAG ]; then
initialize
else
upgrade
fi
run-tests.sh
run_performupgrade)
performupgrade
;;
*) 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

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:
db:
image: postgres
environment:
POSTGRES_DB: mayan
POSTGRES_PASSWORD: mayan-password
POSTGRES_PASSWORD: mayandbpass
POSTGRES_USER: mayan
image: postgres
volumes:
- db:/var/lib/postgresql/data
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
ports:
- 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:
- 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 \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
sudo -u mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py initialsetup
@@ -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 \
MAYAN_DATABASE_PASSWORD=mayanuserpass MAYAN_DATABASE_USER=mayan \
MAYAN_DATABASE_HOST=127.0.0.1 MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
sudo mayan MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'127.0.0.1'}}" \
MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py 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)::
MAYAN_BROKER_URL="redis://127.0.0.1:6379/0",
MAYAN_CELERY_BROKER_URL="redis://127.0.0.1:6379/0",
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)::

View File

@@ -49,12 +49,7 @@ Finally create and run a Mayan EDMS container::
--name mayan-edms \
--restart=always \
-p 80:8000 \
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
-e MAYAN_DATABASE_HOST=172.17.0.1 \
-e MAYAN_DATABASE_NAME=mayan \
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
-e MAYAN_DATABASE_USER=mayan \
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'172.17.0.1'}}" \
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
mayanedms/mayanedms:<version>
@@ -108,12 +103,7 @@ instead of the IP address of the Docker host (``172.17.0.1``)::
--network=mayan \
--restart=always \
-p 80:8000 \
-e MAYAN_DATABASE_ENGINE=django.db.backends.postgresql \
-e MAYAN_DATABASE_HOST=mayan-edms-postgres \
-e MAYAN_DATABASE_NAME=mayan \
-e MAYAN_DATABASE_PASSWORD=mayanuserpass \
-e MAYAN_DATABASE_USER=mayan \
-e MAYAN_DATABASE_CONN_MAX_AGE=0 \
-e MAYAN_DATABASES="{'default':{'ENGINE':'django.db.backends.postgresql','NAME':'mayan','PASSWORD':'mayanuserpass','USER':'mayan','HOST':'mayan-edms-postgres'}}" \
-v /docker-volumes/mayan-edms/media:/var/lib/mayan \
mayanedms/mayanedms:<version>
@@ -137,102 +127,14 @@ To start the container again::
Environment Variables
---------------------
The Mayan EDMS image can be configure via environment variables.
``MAYAN_DATABASE_ENGINE``
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.
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
image specific environment variables are available.
``MAYAN_SETTINGS_MODULE``
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``
Optional. This environment variable controls the number of frontend workers
@@ -275,6 +177,15 @@ Optional. Changes the GID of the ``mayan`` user internal to the Docker
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:
Accessing outside data
@@ -442,6 +353,7 @@ These are:
Nightly images
==============
The continuous integration pipeline used for testing development builds also
produces a resulting Docker image. These are build automatically and their
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
Pass the MAYAN_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
Pass the MAYAN_CELERY_BROKER_URL environment variable (https://kombu.readthedocs.io/en/latest/userguide/connections.html#connection-urls)
to the Mayan EDMS container so that it uses the RabbitMQ container the
message broker::
-e MAYAN_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
-e MAYAN_CELERY_BROKER_URL="amqp://mayan:mayanrabbitmqpassword@localhost:5672/mayan",
When tasks finish, they leave behind a return status or the result of a
calculation, these are stored for a while so that whoever requested the

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

@@ -0,0 +1,213 @@
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.
- Add support for converter layers.
- Add redactions app.
- Unify all line endings to be Linux style.
- Add support for changing the system messages position.
GitLab issue #640. Thanks to Matthias Urhahn (@d4rken).
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:`640` UX: "Toast" Popup position prevents access to actions
- :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::
:maxdepth: 1
3.3
3.2.7
3.2.6
3.2.5

View File

@@ -12,6 +12,7 @@ logger = logging.getLogger(__name__)
class ModelPermission(object):
_functions = {}
_inheritances = {}
_manager_names = {}
_registry = {}
@classmethod
@@ -20,22 +21,6 @@ class ModelPermission(object):
# TODO: Find method to revert the add_to_class('acls'...)
# delattr doesn't work.
@classmethod
def register(cls, model, permissions):
from django.contrib.contenttypes.fields import GenericRelation
cls._registry.setdefault(model, [])
for permission in permissions:
cls._registry[model].append(permission)
AccessControlList = apps.get_model(
app_label='acls', model_name='AccessControlList'
)
model.add_to_class(
name='acls', value=GenericRelation(AccessControlList)
)
@classmethod
def get_classes(cls, as_content_type=False):
ContentType = apps.get_model(
@@ -97,6 +82,40 @@ class ModelPermission(object):
def get_inheritance(cls, 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
def register(cls, model, permissions):
from django.contrib.contenttypes.fields import GenericRelation
cls._registry.setdefault(model, [])
for permission in permissions:
cls._registry[model].append(permission)
AccessControlList = apps.get_model(
app_label='acls', model_name='AccessControlList'
)
model.add_to_class(
name='acls', value=GenericRelation(AccessControlList)
)
@classmethod
def register_function(cls, model, function):
cls._functions[model] = function
@@ -104,3 +123,7 @@ class ModelPermission(object):
@classmethod
def register_inheritance(cls, model, related):
cls._inheritances[model] = related
@classmethod
def register_manager(cls, model, manager_name):
cls._manager_names[model] = manager_name

View File

@@ -45,8 +45,8 @@ class AccessControlListManager(models.Manager):
# 4: No related field, but has an inherited related field, solved by
# recursion, branches to #2 or #3.
# 5: Inherited field of a related field
# -- Not addressed yet --
# 6: Inherited field of a related field that is Generic Foreign Key
# -- Not addressed yet --
# 7: Has a related function
result = []
@@ -58,10 +58,28 @@ class AccessControlListManager(models.Manager):
if isinstance(related_field, GenericForeignKey):
# Case 3: Generic Foreign Key, multiple ContentTypes + object
# id combinations
# Also handles case #6 using the parent related field
# reference template.
# Craft a double underscore reference to a previous related
# field in the case where multiple related fields are
# associated.
# Example: object_layer__content_type
recuisive_related_reference = '__'.join(related_field_name.split('__')[0:-1])
# If there is at least one parent related field we add a
# double underscore to make it a valid filter template.
if recuisive_related_reference:
recuisive_related_reference = '{}__'.format(recuisive_related_reference)
content_type_object_id_queryset = queryset.annotate(
ct_fk_combination=Concat(
related_field.ct_field, Value('-'),
related_field.fk_field, output_field=CharField()
'{}{}'.format(
recuisive_related_reference, related_field.ct_field
), Value('-'),
'{}{}'.format(
recuisive_related_reference, related_field.fk_field
), output_field=CharField()
)
).values('ct_fk_combination')
@@ -75,8 +93,7 @@ class AccessControlListManager(models.Manager):
ct_fk_combination__in=content_type_object_id_queryset
).values('object_id')
field_lookup = 'object_id__in'
field_lookup = '{}object_id__in'.format(recuisive_related_reference)
result.append(Q(**{field_lookup: acl_filter}))
else:
# Case 2: Related field of a single type, single ContentType,
@@ -97,6 +114,7 @@ class AccessControlListManager(models.Manager):
# Case 5: Related field, has an inherited related field itself
# Bubble up permssion check
# Recurse and reduce
# TODO: Add relationship support: OR or AND
# TODO: OR for document pages, version, doc, and types
# TODO: AND for new cabinet levels ACLs
@@ -200,14 +218,11 @@ class AccessControlListManager(models.Manager):
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
# for example the Document model when checking for access for a trashed
# document.
if manager:
source_queryset = manager.all()
else:
meta = getattr(obj, '_meta', None)
if not meta:
@@ -219,9 +234,10 @@ class AccessControlListManager(models.Manager):
)
return True
else:
source_queryset = obj._meta.default_manager.all()
manager = ModelPermission.get_manager(model=obj._meta.model)
source_queryset = manager.all()
restricted_queryset = obj._meta.default_manager.none()
restricted_queryset = manager.none()
for permission in permissions:
# Default relationship betweens permissions is OR
# TODO: Add support for AND relationship

View File

@@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
from rest_framework import status
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
from mayan.apps.rest_api.tests import BaseAPITestCase
from mayan.apps.rest_api.tests.base import BaseAPITestCase
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view

View File

@@ -1,6 +1,6 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.common.tests.base import BaseTestCase
from ..classes import ModelPermission

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.urls import reverse
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.base import GenericViewTestCase
from ..links import (
link_acl_delete, link_acl_list, link_acl_create, link_acl_permissions

View File

@@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals
from django.core.exceptions import PermissionDenied
from django.db import models
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.common.tests.base import BaseTestCase
from ..classes import ModelPermission
from ..models import AccessControlList

View File

@@ -1,6 +1,6 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.base import GenericViewTestCase
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view

View File

@@ -16,7 +16,6 @@ from mayan.apps.permissions.models import Role
from .classes import ModelPermission
from .permissions import permission_acl_edit
__all__ = ('GrantAccessAction', 'RevokeAccessAction')
logger = logging.getLogger(__name__)
@@ -57,7 +56,7 @@ class GrantAccessAction(WorkflowAction):
}
}
field_order = ('content_type', 'object_id', 'roles', 'permissions')
label = _('Grant access')
label = _('Grant object access')
widgets = {
'content_type': {
'class': 'django.forms.widgets.Select', 'kwargs': {
@@ -140,7 +139,7 @@ class GrantAccessAction(WorkflowAction):
class RevokeAccessAction(GrantAccessAction):
label = _('Revoke access')
label = _('Revoke object access')
def execute(self, context):
self.get_execute_data()

View File

@@ -4,6 +4,7 @@ from django.template.loader import get_template
class IconDriver(object):
context = {}
_registry = {}
@classmethod
@@ -14,6 +15,17 @@ class IconDriver(object):
def register(cls, 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):
name = 'fontawesome'
@@ -22,10 +34,8 @@ class FontAwesomeDriver(IconDriver):
def __init__(self, symbol):
self.symbol = symbol
def render(self):
return get_template(template_name=self.template_name).render(
context={'symbol': self.symbol}
)
def get_context(self):
return {'symbol': self.symbol}
class FontAwesomeDualDriver(IconDriver):
@@ -36,9 +46,8 @@ class FontAwesomeDualDriver(IconDriver):
self.primary_symbol = primary_symbol
self.secondary_symbol = secondary_symbol
def render(self):
return get_template(template_name=self.template_name).render(
context={
def get_context(self):
return {
'data': (
{
'class': 'fas fa-circle',
@@ -52,7 +61,6 @@ class FontAwesomeDualDriver(IconDriver):
},
)
}
)
class FontAwesomeCSSDriver(IconDriver):
@@ -62,10 +70,8 @@ class FontAwesomeCSSDriver(IconDriver):
def __init__(self, css_classes):
self.css_classes = css_classes
def render(self):
return get_template(template_name=self.template_name).render(
context={'css_classes': self.css_classes}
)
def get_context(self):
return {'css_classes': self.css_classes}
class FontAwesomeMasksDriver(IconDriver):
@@ -75,23 +81,23 @@ class FontAwesomeMasksDriver(IconDriver):
def __init__(self, data):
self.data = data
def render(self):
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
def get_context(self):
return {'data': self.data}
class FontAwesomeLayersDriver(IconDriver):
name = 'fontawesome-layers'
template_name = 'appearance/icons/font_awesome_layers.html'
def __init__(self, data):
def __init__(self, data, shadow_class=None):
self.data = data
self.shadow_class = shadow_class
def render(self):
return get_template(template_name=self.template_name).render(
context={'data': self.data}
)
def get_context(self):
return {
'data': self.data,
'shadow_class': self.shadow_class,
}
class Icon(object):

View File

@@ -1 +1,2 @@
DEFAULT_MAXIMUM_TITLE_LENGTH = 120
DEFAULT_MESSAGE_POSITION = 'top-right'

View File

@@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.smart_settings.classes import Namespace
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH, DEFAULT_MESSAGE_POSITION
namespace = Namespace(label=_('Appearance'), name='appearance')
@@ -15,3 +15,11 @@ setting_max_title_length = namespace.add_setting(
'title.'
)
)
setting_message_position = namespace.add_setting(
default=DEFAULT_MESSAGE_POSITION,
global_name='APPEARANCE_MESSAGE_POSITION', help_text=_(
'Position where the system message will be displayed. Options are: '
'top-left, top-center, top-right, bottom-left, bottom-center, '
'bottom-right.'
)
)

View File

@@ -12,7 +12,7 @@
}
body {
padding-top: 70px;
padding-top: 60px;
}
.navbar-brand {
@@ -70,7 +70,8 @@ img.lazy-load-carousel {
}
.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 {
@@ -88,19 +89,17 @@ hr {
}
.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;
white-space: normal;
min-height: 120px;
padding-top: 20px;
padding-bottom: 1px;
}
.btn-block .fa {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
}
.btn-block {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
padding-top: 20px;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 1);
white-space: normal;
}
.radio ul li {
@@ -112,14 +111,10 @@ a i {
}
.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;
}
.dashboard-widget .panel-heading i {
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
}
.dashboard-widget-icon {
font-size: 200%;
}
@@ -170,7 +165,7 @@ a i {
}
.navbar-collapse {
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 {
top: 0;
@@ -213,6 +208,22 @@ a i {
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 */
@media (min-width:1200px) {
.container-fluid {
@@ -242,14 +253,6 @@ a i {
margin: auto;
}
.thin_border {
border: 1px solid black;
display: block;
margin-left: auto;
margin-right: auto;
}
.thin_border-thumbnail {
display: block;
max-width: 100%;
@@ -259,10 +262,18 @@ a i {
margin: auto;
}
/* Must go after .thin_border-thumbnail */
.thin_border {
border: 1px solid black;
display: inline;
margin-left: 0px;
margin-right: 0px;
}
#ajax-spinner {
position: fixed;
top: 12px;
right: 10px;
top: 16px;
left: 10px;
z-index: 9999;
width: 25px;
height: 25px;
@@ -328,7 +339,7 @@ a i {
.main {
padding-right: 0px;
padding-left: 0px;
/*margin-left: 210px;*/
margin-left: 210px;
}
}
@@ -410,3 +421,139 @@ a i {
.btn-list {
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({
initialURL: initialURL,
disabledAnchorClasses: ['disabled'],
disabledAnchorClasses: [
'btn-multi-item-action', 'disabled', 'pagination-disabled'
],
excludeAnchorClasses: ['fancybox', 'new_window', 'non-ajax'],
formBeforeSerializeCallbacks: [MayanApp.MultiObjectFormProcess],
});

View File

@@ -4,7 +4,7 @@ class MayanApp {
constructor (options) {
var self = this;
options = options || {
this.options = options || {
ajaxMenusOptions: []
}
@@ -17,28 +17,46 @@ class MayanApp {
// Class methods and variables
static MultiObjectFormProcess ($form, options) {
/*
* ajaxForm callback to add the external item checkboxes to the
* submitted form
*/
static countChecked() {
var checkCount = $('.check-all-slave:checked').length;
if ($form.hasClass('form-multi-object-action')) {
// Turn form data into an object
var formArray = $form.serializeArray().reduce(function (obj, item) {
obj[item.name] = item.value;
return obj;
}, {});
if (checkCount) {
$('#multi-item-title').hide();
$('#multi-item-actions').show();
} else {
$('#multi-item-title').show();
$('#multi-item-actions').hide();
}
}
// Add all checked checkboxes to the form data
$('.form-multi-object-action-checkbox:checked').each(function() {
var $this = $(this);
formArray[$this.attr('name')] = $this.attr('value');
static setupMultiItemActions () {
$('body').on('change', '.check-all-slave', function () {
MayanApp.countChecked();
});
// Set the form data as the data to send
options.data = formArray;
$('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) {
var $this = $(this);
$('.a-main-menu-accordion-link').each(function (index, value) {
$this.parent().removeClass('active');
});
$this.parent().addClass('active');
});
}
static updateNavbarState () {
@@ -46,8 +64,10 @@ class MayanApp {
var uriFragment = uri.fragment();
$('.a-main-menu-accordion-link').each(function (index, value) {
if (value.pathname === uriFragment) {
$(this).closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
$(this).parent().addClass('active');
var $this = $(this);
$this.closest('.collapse').addClass('in').parent().find('.collapsed').removeClass('collapsed').attr('aria-expanded', 'true');
$this.parent().addClass('active');
}
});
}
@@ -92,7 +112,7 @@ class MayanApp {
'closeButton': true,
'debug': false,
'newestOnTop': true,
'positionClass': 'toast-top-right',
'positionClass': 'toast-' + this.options.messagePosition,
'preventDuplicates': false,
'onclick': null,
'showDuration': '300',
@@ -162,17 +182,19 @@ class MayanApp {
var self = this;
this.setupAJAXSpinner();
this.setupAutoSubmit();
this.setupBodyAdjust();
this.setupFormHotkeys();
this.setupFullHeightResizing();
this.setupItemsSelector();
MayanApp.setupMultiItemActions();
this.setupNavbarCollapse();
MayanApp.setupNavBarState();
this.setupNewWindowAnchor();
$.each(this.ajaxMenusOptions, function(index, value) {
value.app = self;
app.doRefreshAJAXMenu(value);
});
this.setupPanelSelection();
partialNavigation.initialize();
}
@@ -196,14 +218,6 @@ class MayanApp {
});
}
setupAutoSubmit () {
$('body').on('change', '.select-auto-submit', function () {
if ($(this).val()) {
$(this.form).trigger('submit');
}
});
}
setupBodyAdjust () {
var self = this;
@@ -242,9 +256,22 @@ class MayanApp {
app.lastChecked = null;
$('body').on('click', '.check-all', function (event) {
var $this = $(this);
var checked = $(event.target).prop('checked');
var $checkBoxes = $('.check-all-slave');
if (checked === undefined) {
checked = $this.data('checked');
checked = !checked;
$this.data('checked', checked);
if (checked) {
$this.find('[data-fa-i2svg]').addClass($this.data('icon-checked')).removeClass($this.data('icon-unchecked'));
} else {
$this.find('[data-fa-i2svg]').addClass($this.data('icon-unchecked')).removeClass($this.data('icon-checked'));
}
}
$checkBoxes.prop('checked', checked);
$checkBoxes.trigger('change');
});
@@ -290,6 +317,58 @@ class MayanApp {
});
}
setupPanelSelection () {
var app = this;
// Setup panel highlighting on check
$('body').on('change', '.check-all-slave', function (event) {
var checked = $(event.target).prop('checked');
if (checked) {
$(this).closest('.panel-item').addClass('panel-highlighted');
} else {
$(this).closest('.panel-item').removeClass('panel-highlighted');
}
});
$('body').on('click', '.panel-item', function (event) {
var $this = $(this);
var targetSrc = $(event.target).prop('src');
var targetHref = $(event.target).prop('href');
var targetIsButton = event.target.tagName === 'BUTTON';
var lastChecked = null;
if ((targetSrc === undefined) && (targetHref === undefined) && (targetIsButton === false)) {
var $checkbox = $this.find('.check-all-slave');
var checked = $checkbox.prop('checked');
if (checked) {
$checkbox.prop('checked', '');
$checkbox.trigger('change');
} else {
$checkbox.prop('checked', 'checked');
$checkbox.trigger('change');
}
if(!app.lastChecked) {
app.lastChecked = $checkbox;
}
if (event.shiftKey) {
var $checkBoxes = $('.check-all-slave');
var start = $checkBoxes.index($checkbox);
var end = $checkBoxes.index(app.lastChecked);
$checkBoxes.slice(
Math.min(start, end), Math.max(start, end) + 1
).prop('checked', app.lastChecked.prop('checked')).trigger('change');
}
app.lastChecked = $checkbox;
window.getSelection().removeAllRanges();
}
});
}
setupScrollView () {
$('.scrollable').scrollview();
}

View File

@@ -23,6 +23,7 @@
{% block content_plain %}{% endblock %}
{% else %}
<div class="">
{% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}
<div class="row zero-margin">
@@ -43,7 +44,7 @@
{% if settings_changed %}
<div class="alert alert-dismissible alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<p><strong>{% trans 'Warning' %}</strong> {% trans 'Settings updated, restart your installation for changes to take proper effect.' %}</p>
<p><strong>{% trans 'Warning' %}</strong> {% trans 'Settings updated, restart your installation and refresh your browser for changes to take effect.' %}</p>
</div>
{% endif %}
</div>
@@ -138,6 +139,9 @@
},
{% endfor %}
];
$(function () {
$('[data-toggle="tooltip"]').tooltip();
})
</script>
{% block javascript %}{% endblock %}

View File

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

View File

@@ -45,7 +45,7 @@
{{ field }}
{% endfor %}
{% for field in form.visible_fields %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<div class="form-group {% if field.errors %}has-error{% endif %} {{ form_field_css_classes }}">
{# We display the label then the field for all except checkboxes #}
{% if field|widget_type != 'checkboxinput' and not field.field.widget.attrs.hidden %}
{% if not hide_labels %}{{ field.label_tag }}{% if field.field.required and not read_only %} ({% trans 'required' %}){% endif %}{% endif %}

View File

@@ -11,41 +11,9 @@
{% include 'appearance/no_results.html' %}
</div>
{% else %}
<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>
{% include "appearance/list_header.html" %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
<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">
{% for object in object_list %}
<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="form-group">
<div class="checkbox">
<label for="id_indexes_0">
{% if multi_item_actions %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" type="checkbox" name="pk_{{ object.pk }}" />
<label for="id_indexes_0" style="cursor: auto;">
{% if links_multi_menus_results %}
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" style="cursor: pointer;" type="checkbox" />
{% endif %}
<span style="color: white; word-break: break-all; overflow-wrap: break-word;">
@@ -68,13 +36,8 @@
{% else %}
{% navigation_get_source_columns source=object only_identifier=True as source_column %}
{% navigation_source_column_resolve column=source_column as 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 %}
</span>
</label>
</div>
@@ -82,11 +45,10 @@
</div>
<div class="panel-body">
{% if not hide_columns %}
{% navigation_get_source_columns source=object exclude_identifier=True as 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 %}
{% endif %}
@@ -136,7 +98,6 @@
</div>
{% endfor %}
</div>
{% include 'pagination/pagination.html' %}
</div>
{% endif %}
</div>

View File

@@ -1,6 +1,7 @@
{% load i18n %}
{% load static %}
{% load appearance_tags %}
{% load common_tags %}
{% load navigation_tags %}
@@ -11,44 +12,16 @@
{% include 'appearance/no_results.html' %}
</div>
{% else %}
<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>
{% include "appearance/list_header.html" %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
<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">
<table class="table table-condensed table-striped">
<tbody>
{% if not hide_header %}
<tr>
{% if multi_item_actions %}
<th class="first"><input class="checkbox check-all" type="checkbox" /></th>
{% if links_multi_menus_results %}
<th class="first"></th>
{% endif %}
{% if not hide_object %}
@@ -58,30 +31,40 @@
{% if source_column %}
<th>
{% 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 icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
{% else %}
{{ 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 %}
</th>
{% endif %}
{% endif %}
{% if not hide_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>
{% if column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=column %}">{{ column.label }}
{% if column.get_sort_field == sort_field %}
{% if source_column.is_sortable %}
<a href="{% navigation_get_sort_field_querystring column=source_column %}">{{ source_column.label }}</a>
{% if source_column.get_sort_field == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
{% 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 %}
</th>
{% endfor %}
@@ -99,9 +82,9 @@
{% for object in object_list %}
<tr>
{% if multi_item_actions %}
{% if links_multi_menus_results %}
<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>
{% endif %}
@@ -112,11 +95,7 @@
{% navigation_source_column_resolve column=source_column as column_value %}
{% if column_value %}
<td>
{% 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 %}
</td>
{% endif %}
{% endif %}
@@ -170,7 +149,6 @@
</tbody>
</table>
</div>
{% include 'pagination/pagination.html' %}
</div>
{% endif %}
</div>

View File

@@ -1,4 +1,7 @@
<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 %}
<i class="{{ entry.class }}" data-fa-transform="{{ entry.transform }}" data-fa-mask="{{ entry.mask }}"></i>
{% 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 smart_settings_tags %}
{% spaceless %}
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<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="icon-bar"></span>
<span class="icon-bar"></span>
@@ -14,9 +15,10 @@
</button>
<a class="navbar-brand" href="{% url home_view %}">{% smart_setting 'COMMON_PROJECT_TITLE' %}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
{% navigation_resolve_menu name='main' as topbar_menus_results %}
<ul class="nav navbar-nav navbar-right">
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
{% for tobpar_menu_result in topbar_menus_results %}
{% for link_group in tobpar_menu_result.link_groups %}
{% for link in link_group.links %}
@@ -34,24 +36,8 @@
{% 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>
</div>
</div>
</nav>
{% endspaceless %}

View File

@@ -32,8 +32,11 @@
{% if appearance_type == 'plain' %}
{% block content_plain %}{% endblock %}
{% else %}
<div id="menu-topbar">
{% include 'appearance/menu_topbar.html' %}
</div>
<div id="menu-main">
{% include 'appearance/main_menu.html' %}
{% include 'appearance/menu_main.html' %}
</div>
<div class="main">
<div class="body-spacer"></div>
@@ -103,6 +106,7 @@
ajaxMenusOptions: [
{
callback: function (options) {
MayanApp.updateNavbarState();
options.app.doBodyAdjust();
},
interval: 5000,
@@ -110,7 +114,14 @@
name: '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" %}'
},
],
messagePosition: '{% smart_setting "APPEARANCE_MESSAGE_POSITION" %}'
});
var afterBaseLoad = function () {

View File

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

View File

@@ -7,6 +7,11 @@ from django.utils.translation import ugettext_lazy as _
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
def get_choice_value(field):
try:

View File

@@ -11,7 +11,7 @@ from django.test import override_settings
from django.urls import reverse
from django.utils.http import urlunquote_plus
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.permissions import permission_user_edit
from mayan.apps.user_management.tests.literals import TEST_USER_PASSWORD_EDITED

View File

@@ -4,21 +4,23 @@
{% if autoadmin_properties.account %}
<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>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">{% trans 'First time login' %}</h3>
<h3 class="text-center panel-title">{% trans 'Automatic credentials' %}</h3>
</div>
<div class="panel-body">
<div class="content login">
{% smart_setting 'COMMON_PROJECT_TITLE' as project_title %}
<p>{% blocktrans %}You have just finished installing <strong>{{ project_title }}</strong>, congratulations!{% endblocktrans %}</p>
<p>{% trans 'Login using the following credentials:' %}</p>
<p>{% blocktrans with autoadmin_properties.account as account %}Username: <strong>{{ account }}</strong>{% endblocktrans %}</p>
<p>{% blocktrans with autoadmin_properties.account.email as email %}Email: <strong>{{ email }}</strong>{% endblocktrans %}</p>
<p>{% blocktrans with autoadmin_properties.password as password %}Password: <strong>{{ password }}</strong>{% endblocktrans %}</p>
<p>{% trans 'Be sure to change the password to increase security and to disable this message.' %}</p>
<p class="text-center">{% blocktrans %}You have just finished installing <strong>{{ project_title }}</strong>, congratulations!{% endblocktrans %}</p>
<p class="text-center">{% trans 'Login using the following credentials:' %}</p>
<p class="text-center">
{% blocktrans with autoadmin_properties.account as account %}Username: <strong>{{ account }}</strong>{% endblocktrans %}<br>
{% blocktrans with autoadmin_properties.account.email as email %}Email: <strong>{{ email }}</strong>{% endblocktrans %}<br>
{% blocktrans with autoadmin_properties.password as password %}Password: <strong>{{ password }}</strong>{% endblocktrans %}
</p>
<p class="text-center">{% trans 'Be sure to change the password to increase security and to disable this message.' %}</p>
</div>
</div>
</div>

View File

@@ -3,5 +3,5 @@ from __future__ import unicode_literals
TEST_ADMIN_USER_EMAIL = 'testemail@example.com'
TEST_ADMIN_USER_PASSWORD = 'test admin user password'
TEST_ADMIN_USER_USERNAME = 'test_admin_user_username'
TEST_FIRST_TIME_LOGIN_TEXT = 'First time login'
TEST_FIRST_TIME_LOGIN_TEXT = 'Automatic credentials'
TEST_MOCK_VIEW_TEXT = 'mock view text'

View File

@@ -2,20 +2,24 @@ from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.core import management
from django.test import TestCase
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton
class AutoAdminManagementCommandTestCase(TestCase):
class AutoAdminManagementCommandTestCase(BaseTestCase):
create_test_case_user = False
def setUp(self):
super(AutoAdminManagementCommandTestCase, self).setUp()
with mute_stdout():
management.call_command('createautoadmin')
def tearDown(self):
AutoAdminSingleton.objects.all().delete()
super(AutoAdminManagementCommandTestCase, self).tearDown()
def test_autoadmin_creation(self):
autoadmin = AutoAdminSingleton.objects.get()

View File

@@ -2,8 +2,7 @@ from __future__ import unicode_literals
import logging
from django.test import TestCase
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton
@@ -12,7 +11,7 @@ from ..settings import setting_username
from .literals import TEST_ADMIN_USER_PASSWORD
class AutoAdminHandlerTestCase(TestCase):
class AutoAdminHandlerTestCase(BaseTestCase):
def test_post_admin_creation(self):
logging.disable(logging.INFO)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mayan.apps.common.settings import setting_home_view
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton

View File

@@ -18,6 +18,7 @@ from mayan.apps.events.permissions import permission_events_view
from mayan.apps.documents.search import document_page_search, document_search
from mayan.apps.navigation.classes import SourceColumn
from .control_codes import * # NOQA
from .dependencies import * # NOQA
from .events import (
event_cabinet_edited, event_cabinet_add_document,

View File

@@ -0,0 +1,31 @@
from __future__ import unicode_literals
from django.apps import apps
from mayan.apps.control_codes.classes import ControlCode
__all__ = ('ControlCodeCabinetDocumentAdd',)
class ControlCodeCabinetDocumentAdd(ControlCode):
arguments = ('label_path',)
label = 'Add document to cabinet'
name = 'cabinet_document_add_v1'
def execute(self, context):
Cabinet = apps.get_model(
app_label='cabinets', model_name='Cabinet'
)
document = context['document_page'].document
user = context.get('user', None)
queryset = Cabinet.objects.all()
for label in self.kwargs['label_path']:
cabinet = queryset.get(label=label)
queryset = cabinet.get_children()
cabinet.document_add(document=document, _user=user)
ControlCode.register(control_code=ControlCodeCabinetDocumentAdd)

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,
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(
blank=True, related_name='cabinets', to=Document,
verbose_name=_('Documents')

View File

@@ -5,8 +5,8 @@ from django.utils.encoding import force_text
from rest_framework import status
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import DocumentTestMixin
from mayan.apps.rest_api.tests import BaseAPITestCase
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from mayan.apps.rest_api.tests.base import BaseAPITestCase
from ..models import Cabinet
from ..permissions import (

View File

@@ -0,0 +1,40 @@
from __future__ import unicode_literals
from mayan.apps.documents.tests.base import GenericDocumentTestCase
from mayan.apps.storage.utils import fs_cleanup, mkstemp
from ..control_codes import ControlCodeCabinetDocumentAdd
from .mixins import CabinetTestMixin, CabinetViewTestMixin
class ControlCodeCabinetDocumentAddTestCase(
CabinetTestMixin, CabinetViewTestMixin,
GenericDocumentTestCase
):
auto_upload_document = False
def setUp(self):
super(ControlCodeCabinetDocumentAddTestCase, self).setUp()
self.test_document_path = mkstemp()[1]
self._create_test_cabinet()
self._create_test_cabinet_child()
def tearDown(self):
fs_cleanup(filename=self.test_document_path)
super(ControlCodeCabinetDocumentAddTestCase, self).tearDown()
def test_control_code(self):
with open(self.test_document_path, mode='wb') as file_object:
control_code = ControlCodeCabinetDocumentAdd(
label_path=(
self.test_cabinet.label, self.test_cabinet_child.label
),
)
control_code.get_image().save(file_object)
self.upload_document()
self.assertEqual(
self.test_document.cabinets.first(), self.test_cabinet_child
)

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from actstream.models import Action
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.documents.tests.test_models import GenericDocumentTestCase
from ..events import (

View File

@@ -2,8 +2,8 @@ from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.tests import DocumentTestMixin
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from ..models import Cabinet

View File

@@ -1,8 +1,8 @@
from __future__ import absolute_import, unicode_literals
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from ..models import Cabinet
from ..permissions import (
@@ -14,7 +14,9 @@ from .literals import TEST_CABINET_LABEL, TEST_CABINET_LABEL_EDITED
from .mixins import CabinetTestMixin, CabinetViewTestMixin
class CabinetViewTestCase(CabinetTestMixin, CabinetViewTestMixin, GenericViewTestCase):
class CabinetViewTestCase(
CabinetTestMixin, CabinetViewTestMixin, GenericViewTestCase
):
def test_cabinet_create_view_no_permission(self):
response = self._request_test_cabinet_create_view()
self.assertEqual(response.status_code, 403)
@@ -105,7 +107,9 @@ class CabinetViewTestCase(CabinetTestMixin, CabinetViewTestMixin, GenericViewTes
)
class CabinetChildViewTestCase(CabinetTestMixin, CabinetViewTestMixin, GenericViewTestCase):
class CabinetChildViewTestCase(
CabinetTestMixin, CabinetViewTestMixin, GenericViewTestCase
):
def setUp(self):
super(CabinetChildViewTestCase, self).setUp()
self._create_test_cabinet()
@@ -157,7 +161,9 @@ class CabinetChildViewTestCase(CabinetTestMixin, CabinetViewTestMixin, GenericVi
self.assertEqual(Cabinet.objects.count(), cabinet_count - 1)
class CabinetDocumentViewTestCase(CabinetTestMixin, CabinetViewTestMixin, GenericDocumentViewTestCase):
class CabinetDocumentViewTestCase(
CabinetTestMixin, CabinetViewTestMixin, GenericDocumentViewTestCase
):
def _add_document_to_cabinet(self):
return self.post(
viewname='cabinets:document_cabinet_add', kwargs={

View File

@@ -2,9 +2,8 @@ from __future__ import unicode_literals
from mayan.apps.documents.models import Document
from mayan.apps.documents.permissions import permission_document_create
from mayan.apps.documents.tests import (
GenericDocumentViewTestCase, TEST_SMALL_DOCUMENT_PATH,
)
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from mayan.apps.sources.models import WebFormSource
from mayan.apps.sources.tests.literals import (
TEST_SOURCE_LABEL, TEST_SOURCE_UNCOMPRESS_N

View File

@@ -12,55 +12,62 @@ from .views import (
CabinetDeleteView, CabinetDetailView, CabinetEditView, CabinetListView,
)
urlpatterns = [
urlpatterns_cabinets = [
url(
regex=r'^list/$', view=CabinetListView.as_view(), name='cabinet_list'
regex=r'^cabinets/$', view=CabinetListView.as_view(), name='cabinet_list'
),
url(
regex=r'^(?P<pk>\d+)/child/add/$', view=CabinetChildAddView.as_view(),
name='cabinet_child_add'
),
url(
regex=r'^create/$', view=CabinetCreateView.as_view(),
regex=r'^cabinets/create/$', view=CabinetCreateView.as_view(),
name='cabinet_create'
),
url(
regex=r'^(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
name='cabinet_edit'
regex=r'^cabinets/(?P<pk>\d+)/children/add/$', view=CabinetChildAddView.as_view(),
name='cabinet_child_add'
),
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'
),
url(
regex=r'^(?P<pk>\d+)/$', view=CabinetDetailView.as_view(),
name='cabinet_view'
regex=r'^cabinets/(?P<pk>\d+)/edit/$', view=CabinetEditView.as_view(),
name='cabinet_edit'
),
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'
),
url(
regex=r'^document/multiple/cabinet/add/$',
regex=r'^documents/multiple/cabinets/add/$',
view=DocumentAddToCabinetView.as_view(),
name='document_multiple_cabinet_add'
),
url(
regex=r'^document/(?P<pk>\d+)/cabinet/remove/$',
regex=r'^documents/(?P<pk>\d+)/cabinets/remove/$',
view=DocumentRemoveFromCabinetView.as_view(),
name='document_cabinet_remove'
),
url(
regex=r'^document/multiple/cabinet/remove/$',
regex=r'^documents/multiple/cabinets/remove/$',
view=DocumentRemoveFromCabinetView.as_view(),
name='multiple_document_cabinet_remove'
),
url(
regex=r'^document/(?P<pk>\d+)/cabinet/list/$',
regex=r'^documents/(?P<pk>\d+)/cabinets/$',
view=DocumentCabinetListView.as_view(), name='document_cabinet_list'
),
]
urlpatterns = []
urlpatterns.extend(urlpatterns_cabinets)
urlpatterns.extend(urlpatterns_documents_cabinets)
api_urls = [
url(
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.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.events.classes import ModelEventType
from mayan.apps.navigation.classes import SourceColumn
from .dashboard_widgets import DashboardWidgetTotalCheckouts
from .events import (
@@ -17,8 +20,9 @@ from .events import (
)
from .handlers import handler_check_new_version_creation
from .links import (
link_check_in_document, link_check_out_document, link_check_out_info,
link_check_out_list
link_check_in_document, link_check_in_document_multiple,
link_check_out_document, link_check_out_document_multiple,
link_check_out_info, link_check_out_list
)
from .methods import (
method_check_in, method_get_check_out_info, method_get_check_out_state,
@@ -43,6 +47,8 @@ class CheckoutsApp(MayanAppConfig):
def ready(self):
super(CheckoutsApp, self).ready()
CheckedOutDocument = self.get_model(model_name='CheckedOutDocument')
DocumentCheckout = self.get_model(model_name='DocumentCheckout')
Document = apps.get_model(
app_label='documents', model_name='Document'
)
@@ -76,6 +82,22 @@ class CheckoutsApp(MayanAppConfig):
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(
widget=DashboardWidgetTotalCheckouts, order=-1
@@ -85,6 +107,22 @@ class CheckoutsApp(MayanAppConfig):
links=(link_check_out_info,), sources=(Document,)
)
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(
links=(link_check_out_document, link_check_in_document),
sources=(

View File

@@ -10,7 +10,7 @@ from .models import DocumentCheckout
from .widgets import SplitTimeDeltaWidget
class DocumentCheckoutForm(forms.ModelForm):
class DocumentCheckOutForm(forms.ModelForm):
class Meta:
fields = ('expiration_datetime', 'block_new_version')
model = DocumentCheckout
@@ -19,7 +19,7 @@ class DocumentCheckoutForm(forms.ModelForm):
}
class DocumentCheckoutDefailForm(DetailForm):
class DocumentCheckOutDetailForm(DetailForm):
def __init__(self, *args, **kwargs):
instance = kwargs['instance']
@@ -56,7 +56,7 @@ class DocumentCheckoutDefailForm(DetailForm):
)
kwargs['extra_fields'] = extra_fields
super(DocumentCheckoutDefailForm, self).__init__(*args, **kwargs)
super(DocumentCheckOutDetailForm, self).__init__(*args, **kwargs)
class Meta:
fields = ()

View File

@@ -38,16 +38,26 @@ link_check_out_document = Link(
args='object.pk', condition=is_not_checked_out,
icon_class=icon_check_out_document,
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(
args='object.pk', icon_class=icon_check_in_document,
condition=is_checked_out, permissions=(
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(
args='resolved_object.pk', icon_class=icon_check_out_info, permissions=(
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.utils.timezone import now
from mayan.apps.acls.models import AccessControlList
from mayan.apps.documents.models import Document
from .events import (
@@ -14,10 +15,58 @@ from .events import (
)
from .exceptions import DocumentNotCheckedOut
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__)
class DocumentCheckoutBusinessLogicManager(models.Manager):
def check_in_document(self, document, user=None):
# Convert any document submodel to the parent model class
queryset = document._meta.default_manager.filter(pk=document.pk)
if not self.filter(document__pk__in=queryset).exists():
raise DocumentNotCheckedOut
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):
def are_document_new_versions_allowed(self, document, user=None):
try:
@@ -27,25 +76,6 @@ class DocumentCheckoutManager(models.Manager):
else:
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):
for document in self.expired_check_outs():
document.check_in()
@@ -57,7 +87,11 @@ class DocumentCheckoutManager(models.Manager):
)
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')
)
@@ -74,7 +108,11 @@ class DocumentCheckoutManager(models.Manager):
return STATE_CHECKED_IN
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(
expiration_datetime__lte=now()
).values_list('document__pk', flat=True)
@@ -83,9 +121,6 @@ class DocumentCheckoutManager(models.Manager):
return expired_list
def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try:
document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist:

View File

@@ -8,7 +8,7 @@ def method_check_in(self, user=None):
app_label='checkouts', model_name='DocumentCheckout'
)
return DocumentCheckout.objects.check_in_document(
return DocumentCheckout.business_logic.check_in_document(
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 .exceptions import DocumentAlreadyCheckedOut
from .managers import DocumentCheckoutManager, NewVersionBlockManager
from .managers import (
DocumentCheckoutBusinessLogicManager, DocumentCheckoutManager,
NewVersionBlockManager
)
logger = logging.getLogger(__name__)
@@ -49,6 +52,7 @@ class DocumentCheckout(models.Model):
)
objects = DocumentCheckoutManager()
business_logic = DocumentCheckoutBusinessLogicManager()
class Meta:
ordering = ('pk',)
@@ -81,13 +85,13 @@ class DocumentCheckout(models.Model):
natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs):
new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out():
is_new = not self.pk
if not is_new or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut
with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout:
if is_new:
event_document_check_out.commit(
actor=self.user, target=self.document
)
@@ -119,3 +123,24 @@ class NewVersionBlock(models.Model):
def natural_key(self):
return self.document.natural_key()
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

@@ -5,6 +5,7 @@ import datetime
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
@@ -64,16 +65,13 @@ class DocumentCheckoutViewTestMixin(object):
}
)
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_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_list_view(self):
return self.get(viewname='checkouts:check_out_list')
def _request_test_document_check_out_view(self):
return self.post(
viewname='checkouts:check_out_document', kwargs={
@@ -84,3 +82,23 @@ class DocumentCheckoutViewTestMixin(object):
'block_new_version': True
}
)
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_unit': TIME_DELTA_UNIT_DAYS,
'expiration_datetime_amount': 99,
'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

@@ -4,9 +4,9 @@ from django.utils.encoding import force_text
from rest_framework import status
from mayan.apps.documents.tests import DocumentTestMixin
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.rest_api.tests import BaseAPITestCase
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from mayan.apps.rest_api.tests.base import BaseAPITestCase
from ..models import DocumentCheckout
from ..permissions import (

View File

@@ -1,6 +1,6 @@
from __future__ import unicode_literals
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from ..links import link_check_out_document, link_check_out_info
from ..permissions import (

View File

@@ -2,11 +2,10 @@ from __future__ import unicode_literals
import time
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.tests import (
GenericDocumentTestCase, DocumentTestMixin
)
from mayan.apps.common.tests.base import BaseTestCase
from mayan.apps.documents.tests.base import GenericDocumentTestCase
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from mayan.apps.documents.tests.mixins import DocumentTestMixin
from ..exceptions import (
DocumentAlreadyCheckedOut, DocumentNotCheckedOut,
@@ -53,7 +52,7 @@ class DocumentCheckoutTestCase(
block_new_version=True
)
def test_document_checkin_without_checkout(self):
def test_document_check_in_without_check_out(self):
with self.assertRaises(DocumentNotCheckedOut):
self.test_document.check_in()

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from mayan.apps.sources.links import link_document_version_upload
from ..literals import STATE_CHECKED_OUT, STATE_LABELS
@@ -22,8 +22,8 @@ class DocumentCheckoutViewTestCase(
self._check_out_test_document()
response = self._request_test_document_check_in_get_view()
self.assertContains(
response=response, text=self.test_document.label, status_code=200
self.assertNotContains(
response=response, text=self.test_document.label, status_code=404
)
self.assertTrue(self.test_document.is_checked_out())
@@ -46,7 +46,7 @@ class DocumentCheckoutViewTestCase(
self._check_out_test_document()
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())
@@ -67,9 +67,89 @@ class DocumentCheckoutViewTestCase(
)
)
def test_document_multiple_check_in_post_view_no_permission(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])
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_document_multiple_check_in_post_view_with_document_0_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
)
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, 403)
self.assertEqual(response.status_code, 404)
self.assertFalse(self.test_document.is_checked_out())
@@ -87,6 +167,102 @@ class DocumentCheckoutViewTestCase(
self.assertTrue(self.test_document.is_checked_out())
def test_document_multiple_check_out_post_view_no_permission(self):
# Upload second document
self.upload_document()
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, 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_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()
@@ -156,19 +332,18 @@ class DocumentCheckoutViewTestCase(
'pk': self.test_document.pk
}
)
self.assertContains(
response=response, text='Insufficient permissions', status_code=403
)
self.assertEqual(response.status_code, 302)
self.assertTrue(self.test_document.is_checked_out())
def test_document_check_in_forcefull_view_with_permission(self):
def test_document_check_in_forcefull_view_with_access(self):
self._create_test_user()
# Check out document as test_user
self._check_out_test_document(user=self.test_user)
self.grant_access(
obj=self.test_document, permission=permission_document_check_in_override
obj=self.test_document,
permission=permission_document_check_in_override
)
# Check in document as test_case_user
@@ -219,6 +394,8 @@ class NewVersionBlockViewTestCase(
# Needed by the url view resolver
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)

View File

@@ -4,8 +4,8 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import (
DocumentCheckOutView, DocumentCheckOutDetailView, DocumentCheckOutListView,
DocumentCheckInView
DocumentCheckInView, DocumentCheckOutDetailView, DocumentCheckOutListView,
DocumentCheckOutView
)
urlpatterns = [
@@ -14,16 +14,25 @@ urlpatterns = [
name='check_out_list'
),
url(
regex=r'^documents/(?P<pk>\d+)/check/out/$', view=DocumentCheckOutView.as_view(),
name='check_out_document'
regex=r'^documents/(?P<pk>\d+)/check/in/$',
view=DocumentCheckInView.as_view(), name='check_in_document'
),
url(
regex=r'^documents/(?P<pk>\d+)/check/in/$', view=DocumentCheckInView.as_view(),
name='check_in_document'
regex=r'^documents/multiple/check/in/$',
name='check_in_document_multiple', view=DocumentCheckInView.as_view()
),
url(
regex=r'^documents/(?P<pk>\d+)/check/info/$', view=DocumentCheckOutDetailView.as_view(),
name='check_out_info'
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,21 +1,17 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutForm, DocumentCheckoutDefailForm
from .forms import DocumentCheckOutForm, DocumentCheckOutDetailForm
from .icons import icon_check_out_info
from .models import DocumentCheckout
from .permissions import (
@@ -24,66 +20,125 @@ from .permissions import (
)
class DocumentCheckInView(ConfirmView):
class DocumentCheckInView(MultipleObjectConfirmActionView):
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
model = Document
pk_url_kwarg = 'pk'
success_message_singular = '%(count)d document checked in.'
success_message_plural = '%(count)d documents checked in.'
def get_extra_context(self):
document = self.get_object()
queryset = self.get_object_list()
context = {
'object': document,
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
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
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
return context
return result
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:check_out_info', kwargs={
'pk': self.get_object().pk
viewname='checkouts:document_checkout_info',
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
)
check_in_override_queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_check_in_override,
queryset=source_queryset, user=self.request.user
)
return check_in_queryset | check_in_override_queryset
def object_action(self, form, instance):
DocumentCheckout.business_logic.check_in_document(
document=instance, user=self.request.user
)
class DocumentCheckOutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckOutForm
model = Document
object_permission = permission_document_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()
}
)
def view_action(self):
document = self.get_object()
return result
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
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:
AccessControlList.objects.check_access(
obj=document,
permissions=(permission_document_check_in_override,),
user=self.request.user
)
super(DocumentCheckOutView, self).get_post_action_redirect()
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
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 = DocumentCheckOutDetailForm
model = Document
object_permission = permission_document_check_out_detail_view
@@ -96,55 +151,6 @@ class DocumentCheckOutDetailView(SingleObjectDetailView):
}
class DocumentCheckOutView(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(
DocumentCheckOutView, 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):
return {
'object': self.document,
'title': _('Check out document: %s') % self.document
}
def get_post_action_redirect(self):
return reverse(
viewname='checkouts:check_out_info', kwargs={
'pk': self.document.pk
}
)
class DocumentCheckOutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
@@ -157,34 +163,13 @@ class DocumentCheckOutListView(DocumentListView):
context = super(DocumentCheckOutListView, self).get_extra_context()
context.update(
{
'extra_columns': (
{
'name': _('User'),
'attribute': encapsulate(
lambda document: document.get_check_out_info().user.get_full_name() or document.get_check_out_info().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.'
'Checking out a document, blocks certain operations '
'for a predetermined amount of time.'
),
'no_results_title': _('No documents have been checked out'),
'title': _('Documents checked out'),
'title': _('Checked out documents'),
}
)
return context

View File

@@ -27,9 +27,7 @@ from .links import (
)
from .literals import MESSAGE_SQLITE_WARNING
from .menus import (
menu_about, menu_main, menu_secondary, menu_user
)
from .menus import menu_about, menu_secondary, menu_topbar, menu_user
from .settings import (
setting_auto_logging, setting_production_error_log_path,
setting_production_error_logging
@@ -97,7 +95,10 @@ class CommonApp(MayanAppConfig):
)
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(
@@ -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(
links=(link_object_error_list_clear,), sources=(
'common:object_error_list',

View File

@@ -61,102 +61,9 @@ PythonDependency(
SOFTWARE.
''', 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(
module=__name__, name='django-downloadview', version_string='==1.9'
)
PythonDependency(
module=__name__, name='django-environ', version_string='==0.4.5'
)
PythonDependency(
module=__name__, name='django-formtools', version_string='==2.1'
)
@@ -383,6 +290,10 @@ PythonDependency(
module=__name__, environment=environment_development, name='Werkzeug',
version_string='==0.15.4'
)
PythonDependency(
module=__name__, environment=environment_development, name='devpi-server',
version_string='==5.0.0'
)
PythonDependency(
environment=environment_development, module=__name__,
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.utils.encoding import force_bytes
from django.utils.six import PY3
class URL(object):
@@ -20,9 +21,7 @@ class URL(object):
def to_string(self):
if self._args.keys():
query = force_bytes(
'?{}'.format(self._args.urlencode())
)
query = '?{}'.format(self._args.urlencode())
else:
query = ''
@@ -31,6 +30,9 @@ class URL(object):
else:
path = ''
result = force_bytes('{}{}'.format(path, query))
result = '{}{}'.format(path, query)
if PY3:
return result
else:
return force_bytes(result)

View File

@@ -35,8 +35,14 @@ icon_menu_about = Icon(
icon_menu_user = Icon(
driver_name='fontawesome', symbol='user-circle'
)
icon_object_error_list_with_icon = Icon(
driver_name='fontawesome', symbol='lock'
icon_object_errors = Icon(
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(
driver_name='fontawesome', symbol='check'

View File

@@ -8,8 +8,8 @@ from mayan.apps.navigation.classes import Link
from .icons import (
icon_about, icon_current_user_locale_profile_details,
icon_current_user_locale_profile_edit, icon_documentation,
icon_forum, icon_license, icon_object_error_list_with_icon,
icon_setup, icon_source_code, icon_support, icon_tools
icon_forum, icon_license, icon_setup, icon_source_code, icon_support,
icon_tools
)
from .permissions_runtime import permission_error_log_view
@@ -50,21 +50,17 @@ link_documentation = Link(
text=_('Documentation'), url='https://docs.mayan-edms.com'
)
link_object_error_list = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list',
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
icon_class_path='mayan.apps.common.icons.icon_object_error_list_clear',
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'),
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(
icon_class=icon_forum, tags='new_window', text=_('Forum'),
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(
'--no-javascript', action='store_true', dest='no_javascript',
help='Don\'t download the JavaScript dependencies.',
'--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download dependencies.',
)
def initialize_system(self, force=False):
@@ -88,9 +88,9 @@ class Command(management.BaseCommand):
self.initialize_system(force=options.get('force', False))
pre_initial_setup.send(sender=self)
if not options.get('no_javascript', False):
if not options.get('no_dependencies', False):
management.call_command(
command_name='installjavascript', interactive=False
command_name='installdependencies', interactive=False
)
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):
parser.add_argument(
'--no-javascript', action='store_true', dest='no_javascript',
help='Don\'t download the JavaScript dependencies.',
'--no-dependencies', action='store_true', dest='no_dependencies',
help='Don\'t download dependencies.',
)
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(
command_name='installjavascript', interactive=False
command_name='installdependencies', interactive=False
)
try:

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.core import management
from djcelery.models import IntervalSchedule, PeriodicTask
from django_celery_beat.models import IntervalSchedule, PeriodicTask
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_setup = Menu(name='setup')
menu_tools = Menu(name='tools')
menu_topbar = Menu(name='topbar')
menu_user = Menu(
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 django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http import Http404, HttpResponseRedirect
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.views.generic.detail import SingleObjectMixin
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.models import AccessControlList
from mayan.apps.permissions import Permission
@@ -17,6 +19,28 @@ from .literals import PK_LIST_SEPARATOR
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):
"""
Mixin to populate the extra data needed for delete views
@@ -103,7 +127,15 @@ class ExternalObjectMixin(object):
'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):
queryset = self.get_external_object_queryset()
@@ -118,6 +150,20 @@ class ExternalObjectMixin(object):
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):
"""
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):
return ungettext(
self.success_message,
self.success_message_plural,
count
singular=self.success_message,
plural=self.success_message_plural,
number=count
) % {
'count': count,
}
@@ -271,14 +317,15 @@ class ObjectActionMixin(object):
pass
except ActionError:
messages.error(
self.request, self.error_message % {'instance': instance}
message=self.error_message % {'instance': instance},
request=self.request
)
else:
self.action_count += 1
messages.success(
self.request,
self.get_success_message(count=self.action_count)
message=self.get_success_message(count=self.action_count),
request=self.request
)
@@ -383,6 +430,21 @@ class RestrictedQuerysetMixin(object):
object_permission = None
source_queryset = None
def get_object_permission(self):
return self.object_permission
def get_queryset(self):
queryset = self.get_source_queryset()
object_permission = self.get_object_permission()
if object_permission:
queryset = AccessControlList.objects.restrict_queryset(
permission=object_permission, queryset=queryset,
user=self.request.user
)
return queryset
def get_source_queryset(self):
if self.source_queryset is None:
if self.model:
@@ -398,17 +460,6 @@ class RestrictedQuerysetMixin(object):
return self.source_queryset.all()
def get_queryset(self):
queryset = self.get_source_queryset()
if self.object_permission:
queryset = AccessControlList.objects.restrict_queryset(
permission=self.object_permission, queryset=queryset,
user=self.request.user
)
return queryset
class ViewPermissionCheckMixin(object):
"""
@@ -420,11 +471,16 @@ class ViewPermissionCheckMixin(object):
view_permission = None
def dispatch(self, request, *args, **kwargs):
if self.view_permission:
view_permission = self.get_view_permission()
if view_permission:
Permission.check_user_permissions(
permissions=(self.view_permission,), user=self.request.user
permissions=(view_permission,),
user=self.request.user
)
return super(
ViewPermissionCheckMixin, self
).dispatch(request, *args, **kwargs)
def get_view_permission(self):
return self.view_permission

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
namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting(
@@ -95,322 +94,5 @@ setting_shared_storage = namespace.add_setting(
)
setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default='{{location: {}}}'.format(
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'
)
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
)

View File

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

View File

@@ -1,2 +0,0 @@
from .base import BaseTestCase, GenericViewTestCase # NOQA
from .decorators import skip_file_descriptor_check # NOQA

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