Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8acfdde502 | ||
|
|
22900384a4 | ||
|
|
a43dd4fdaa | ||
|
|
162ce96d1b | ||
|
|
3997443bfa | ||
|
|
d2e90b9188 | ||
|
|
c014986a29 | ||
|
|
4e0a5af74e | ||
|
|
e899a6104c | ||
|
|
9cd8c9bdd4 | ||
|
|
e398c8474d | ||
|
|
75ca8fd0d8 | ||
|
|
60f7d09d4a | ||
|
|
3d61eb0044 |
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
"plugins": ["transform-runtime"],
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": [
|
||||
"lodash"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"plugins": [
|
||||
"rewire"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
[run]
|
||||
source =
|
||||
cms
|
||||
menus
|
||||
branch = False
|
||||
omit =
|
||||
cms/migrations/*
|
||||
cms/south_migrations/*
|
||||
cms/tests/*
|
||||
cms/test_utils/*
|
||||
menus/migrations/*
|
||||
menus/south_migrations/*
|
||||
docs/*
|
||||
env/*
|
||||
env*
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if __name__ == .__main__.:
|
||||
@@ -1,3 +0,0 @@
|
||||
repo_token: 4byOdcHPvnUGJ5DL2cwLZccI5HUKKxkVJ
|
||||
service_name: travis-ci
|
||||
parallel: true
|
||||
@@ -1,30 +0,0 @@
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.py]
|
||||
max_line_length = 120
|
||||
quote_type = single
|
||||
|
||||
[*.{scss,js,html}]
|
||||
max_line_length = 120
|
||||
indent_style = space
|
||||
quote_type = double
|
||||
|
||||
[*.js]
|
||||
max_line_length = 120
|
||||
quote_type = single
|
||||
|
||||
[*.rst]
|
||||
max_line_length = 80
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
-233
@@ -1,233 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jquery: true,
|
||||
jasmine: true,
|
||||
es6: true
|
||||
},
|
||||
globals: {
|
||||
CMS: true,
|
||||
Promise: true,
|
||||
__CMS_VERSION__: true
|
||||
},
|
||||
root: true,
|
||||
ecmaFeatures: {
|
||||
modules: true
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: ['compat'],
|
||||
settings: {
|
||||
polyfills: ['document-currentscript']
|
||||
},
|
||||
rules: {
|
||||
// Possible Errors
|
||||
'comma-dangle': [2, 'never'],
|
||||
'no-cond-assign': 2,
|
||||
'no-console': 1,
|
||||
'no-constant-condition': 2,
|
||||
'no-control-regex': 2,
|
||||
'no-debugger': 2,
|
||||
'no-dupe-args': 2,
|
||||
'no-dupe-keys': 2,
|
||||
'no-duplicate-case': 2,
|
||||
'no-empty-character-class': 2,
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
'no-ex-assign': 2,
|
||||
'no-extra-boolean-cast': 2,
|
||||
'no-extra-parens': 0,
|
||||
'no-extra-semi': 2,
|
||||
'no-func-assign': 2,
|
||||
'no-inner-declarations': 2,
|
||||
'no-invalid-regexp': 2,
|
||||
'no-irregular-whitespace': 2,
|
||||
'no-negated-in-lhs': 2,
|
||||
'no-obj-calls': 2,
|
||||
'no-regex-spaces': 2,
|
||||
'no-sparse-arrays': 2,
|
||||
'no-unexpected-multiline': 2,
|
||||
'no-unreachable': 2,
|
||||
'use-isnan': 2,
|
||||
'valid-jsdoc': [
|
||||
2,
|
||||
{
|
||||
requireReturn: false,
|
||||
requireParamDescription: false,
|
||||
requireReturnDescription: false,
|
||||
prefer: {
|
||||
return: 'returns'
|
||||
}
|
||||
}
|
||||
],
|
||||
'valid-typeof': 2,
|
||||
|
||||
// Best Practices
|
||||
'accessor-pairs': 2,
|
||||
'block-scoped-var': 2,
|
||||
complexity: ['error', { max: 10 }],
|
||||
'consistent-return': 0,
|
||||
curly: 2,
|
||||
'default-case': 2,
|
||||
'dot-location': [2, 'property'],
|
||||
'dot-notation': 2,
|
||||
eqeqeq: 2,
|
||||
'guard-for-in': 2,
|
||||
'no-alert': 2,
|
||||
'no-caller': 2,
|
||||
'no-case-declarations': 2,
|
||||
'no-div-regex': 2,
|
||||
'no-else-return': 1,
|
||||
'no-empty-pattern': 2,
|
||||
'no-eq-null': 2,
|
||||
'no-eval': 2,
|
||||
'no-extend-native': 2,
|
||||
'no-extra-bind': 2,
|
||||
'no-fallthrough': 2,
|
||||
'no-floating-decimal': 2,
|
||||
'no-implicit-coercion': 0,
|
||||
'no-implied-eval': 2,
|
||||
'no-invalid-this': 0,
|
||||
'no-iterator': 2,
|
||||
'no-labels': 2,
|
||||
'no-lone-blocks': 2,
|
||||
'no-loop-func': 2,
|
||||
'no-magic-numbers': [
|
||||
'error',
|
||||
{
|
||||
ignore: [0, -1, 1, 2],
|
||||
ignoreArrayIndexes: true
|
||||
}
|
||||
],
|
||||
'no-multi-spaces': 2,
|
||||
'no-multi-str': 0,
|
||||
'no-native-reassign': 2,
|
||||
'no-new-func': 2,
|
||||
'no-new-wrappers': 2,
|
||||
'no-new': 0,
|
||||
'no-octal-escape': 2,
|
||||
'no-octal': 2,
|
||||
'no-param-reassign': 2,
|
||||
'no-process-env': 0,
|
||||
'no-proto': 2,
|
||||
'no-redeclare': 2,
|
||||
'no-return-assign': 2,
|
||||
'no-script-url': 2,
|
||||
'no-self-compare': 2,
|
||||
'no-sequences': 2,
|
||||
'no-throw-literal': 2,
|
||||
'no-unused-expressions': [2, { allowShortCircuit: true }],
|
||||
'no-useless-call': 2,
|
||||
'no-useless-concat': 2,
|
||||
'no-void': 2,
|
||||
'no-warning-comments': 0,
|
||||
'no-with': 2,
|
||||
radix: 2,
|
||||
'vars-on-top': 0, // FIXME should be enabled at some point
|
||||
'wrap-iife': [2, 'inside'],
|
||||
yoda: [2, 'never', { exceptRange: true }],
|
||||
|
||||
// Strict Mode
|
||||
strict: 0, // not required with webpack
|
||||
|
||||
// Variables
|
||||
'init-declarations': 0,
|
||||
'no-catch-shadow': 2,
|
||||
'no-delete-var': 2,
|
||||
'no-label-var': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'no-shadow': 2,
|
||||
'no-undef-init': 2,
|
||||
'no-undef': 2,
|
||||
'no-undefined': 0,
|
||||
'no-unused-vars': 2,
|
||||
'no-use-before-define': 2,
|
||||
|
||||
// Stylistic Issues
|
||||
'array-bracket-spacing': [2, 'never'],
|
||||
'block-spacing': 2,
|
||||
'brace-style': [2, '1tbs'],
|
||||
camelcase: 0,
|
||||
'comma-spacing': [2, { before: false, after: true }],
|
||||
'comma-style': [2, 'last'],
|
||||
'computed-property-spacing': [2, 'never'],
|
||||
'consistent-this': [2, 'that'],
|
||||
'eol-last': 2,
|
||||
'func-names': 0,
|
||||
'func-style': 0,
|
||||
'id-length': 0,
|
||||
'id-match': 0,
|
||||
indent: [
|
||||
'error',
|
||||
4,
|
||||
{
|
||||
SwitchCase: 1
|
||||
}
|
||||
],
|
||||
'jsx-quotes': 0,
|
||||
'key-spacing': [2, { beforeColon: false, afterColon: true }],
|
||||
'linebreak-style': [2, 'unix'],
|
||||
'lines-around-comment': 0,
|
||||
'max-nested-callbacks': [2, 5],
|
||||
'new-cap': 2,
|
||||
'new-parens': 2,
|
||||
'newline-after-var': 2,
|
||||
'no-array-constructor': 2,
|
||||
'no-continue': 2,
|
||||
'no-inline-comments': 0,
|
||||
'no-lonely-if': 2,
|
||||
'no-mixed-spaces-and-tabs': 2,
|
||||
'no-multiple-empty-lines': [2, { max: 2 }],
|
||||
'no-negated-condition': 2,
|
||||
'no-nested-ternary': 2,
|
||||
'no-new-object': 2,
|
||||
'no-restricted-syntax': 0,
|
||||
'no-spaced-func': 0,
|
||||
'no-ternary': 0,
|
||||
'no-trailing-spaces': 2,
|
||||
'no-underscore-dangle': 0,
|
||||
'no-unneeded-ternary': 2,
|
||||
'object-curly-spacing': [
|
||||
2,
|
||||
'always',
|
||||
{
|
||||
objectsInObjects: true,
|
||||
arraysInObjects: true
|
||||
}
|
||||
],
|
||||
'one-var': [2, 'never'],
|
||||
'operator-assignment': 2,
|
||||
'operator-linebreak': 0,
|
||||
'padded-blocks': 0,
|
||||
'quote-props': [2, 'as-needed'],
|
||||
quotes: [2, 'single', 'avoid-escape'],
|
||||
'require-jsdoc': 2,
|
||||
'semi-spacing': [2, { before: false, after: true }],
|
||||
semi: [2, 'always'],
|
||||
'sort-vars': 0,
|
||||
'keyword-spacing': 2,
|
||||
'space-before-blocks': 2,
|
||||
// FIXME reenable after running prettier on full codebase
|
||||
'space-before-function-paren': [0, { anonymous: 'never', named: 'never' }],
|
||||
'space-in-parens': [2, 'never'],
|
||||
'space-infix-ops': 2,
|
||||
'space-unary-ops': 2,
|
||||
'spaced-comment': 2,
|
||||
'wrap-regex': 0,
|
||||
|
||||
// ES6
|
||||
'arrow-parens': [2, 'as-needed'],
|
||||
|
||||
// Legacy
|
||||
'max-depth': [2, 4],
|
||||
'max-len': [2, 120],
|
||||
'max-params': [2, 3],
|
||||
'max-statements': 0,
|
||||
'no-bitwise': 2,
|
||||
'no-plusplus': 0,
|
||||
|
||||
'compat/compat': 2
|
||||
}
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
##########################
|
||||
Contributing to django CMS
|
||||
##########################
|
||||
|
||||
Like every open-source project, django CMS always welcomes contributions.
|
||||
|
||||
|
||||
*******************************
|
||||
GitHub issues and pull requests
|
||||
*******************************
|
||||
|
||||
`GitHub issues <https://github.com/divio/django-cms/issues>`_ should only be used to report *suspected issues* in
|
||||
django CMS.
|
||||
|
||||
It should **not** be used for reporting *security issues* - please use `security@django-cms.org
|
||||
<security@django-cms.org>`_ for those.
|
||||
|
||||
For *help with django CMS*, please use our `users' email list <https://groups.google.com/forum/#!forum/django-cms>`_.
|
||||
|
||||
For *feature requests*, please use our `developers' email list
|
||||
<https://groups.google.com/forum/#!forum/django-cms-developers>`_.
|
||||
|
||||
|
||||
**************************
|
||||
Contribution documentation
|
||||
**************************
|
||||
|
||||
We maintain comprehensive `contribution documentation <http://docs.django-cms.org/en/latest/contributing/>`_ - please
|
||||
familiarise yourself with it.
|
||||
|
||||
|
||||
***************
|
||||
Security issues
|
||||
***************
|
||||
|
||||
Security issues should not be reported **anywhere** except via our dedicated and private `security@django-cms.org
|
||||
<security@django-cms.org>`_ email address.
|
||||
|
||||
If you think you have discovered a security issue in our code, please do not raise it on
|
||||
|
||||
* IRC
|
||||
* GitHub
|
||||
* Twitter
|
||||
* either of our email lists
|
||||
|
||||
or in any other public forum until we have had a chance to deal with it.
|
||||
|
||||
|
||||
***************
|
||||
Code of conduct
|
||||
***************
|
||||
|
||||
django CMS is governed by a `Code of Conduct
|
||||
<http://docs.django-cms.org/en/latest/contributing/code_of_conduct.html>`_. All participants in our community and its
|
||||
various forums are expected to abide by it.
|
||||
|
||||
|
||||
*********
|
||||
Community
|
||||
*********
|
||||
|
||||
You can join us online:
|
||||
|
||||
* in our IRC channel, #django-cms, on ``irc.freenode.net``. If you don't have an IRC client, you can
|
||||
`join our IRC channel using the KiwiIRC web client
|
||||
<https://kiwiirc.com/client/irc.freenode.net/django-cms>`_, which works pretty well.
|
||||
* on our `django CMS users email list <https://groups.google.com/forum/#!forum/django-cms>`_ for
|
||||
**general** django CMS questions and discussion
|
||||
* on our `django CMS developers email list
|
||||
<https://groups.google.com/forum/#!forum/django-cms-developers>`_ for discussions about the
|
||||
**development of django CMS**
|
||||
|
||||
You can also follow:
|
||||
|
||||
* the `Travis Continuous Integration build reports <https://travis-ci.org/divio/django-cms>`_
|
||||
* the `@djangocms`_ Twitter account for general announcements
|
||||
|
||||
.. _@djangocms : https://twitter.com/djangocms
|
||||
@@ -1,30 +0,0 @@
|
||||
<!-- Important!
|
||||
|
||||
Only use this form if you suspect that you have discovered an issue in django CMS.
|
||||
|
||||
Do not use this form for reporting security issues - please use security@django-cms.org for those.
|
||||
|
||||
For help with django CMS, please use our users' email list: https://groups.google.com/forum/#!forum/django-cms.
|
||||
|
||||
For feature requests, please use our developers' email list: https://groups.google.com/forum/#!forum/django-cms-developers.
|
||||
|
||||
-->
|
||||
|
||||
### Summary
|
||||
|
||||
<!-- If this is a security issue stop right here and email security@django-cms.org instead -->
|
||||
|
||||
|
||||
### Expected behaviour
|
||||
|
||||
|
||||
|
||||
### Actual behaviour
|
||||
|
||||
|
||||
|
||||
### Environment
|
||||
|
||||
* Python version:
|
||||
* Django version:
|
||||
* django CMS version:
|
||||
@@ -1,33 +0,0 @@
|
||||
<!--
|
||||
If this is a security-related patch stop immediately!
|
||||
|
||||
See http://docs.django-cms.org/en/latest/contributing/development-policies.html
|
||||
-->
|
||||
|
||||
|
||||
### Summary
|
||||
|
||||
Fixes #
|
||||
|
||||
|
||||
|
||||
### Links to related discussion
|
||||
|
||||
|
||||
|
||||
### Proposed changes in this pull request
|
||||
|
||||
|
||||
### Documentation checklist
|
||||
|
||||
* [ ] I have updated CHANGELOG.txt if appropriate
|
||||
* [ ] I have updated the release notes document if appropriate, with:
|
||||
* [ ] general notes
|
||||
* [ ] bug-fixes
|
||||
* [ ] improvements/new features
|
||||
* [ ] backwards-incompatible changes
|
||||
* [ ] required upgrade steps
|
||||
* [ ] names of contributors
|
||||
* [ ] I have updated other documentation
|
||||
* [ ] I have added my name to the AUTHORS file
|
||||
* [ ] This PR's documentation has been approved by Daniele Procida
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
*.pyc
|
||||
!.travis.yml
|
||||
*.swp
|
||||
*.lock
|
||||
*.log
|
||||
*.pid
|
||||
cms/django
|
||||
*.DS_Store
|
||||
*.svn
|
||||
*.xml
|
||||
/*env*/
|
||||
/*docs/env*/
|
||||
/*docs/build*/
|
||||
*.sqlite
|
||||
cms/media/cms_page_media/
|
||||
example/run
|
||||
example/local_settings.py
|
||||
reversion/
|
||||
htmlcov
|
||||
build
|
||||
dist
|
||||
.ropeproject
|
||||
.project
|
||||
.coverage
|
||||
.pydevproject
|
||||
.vscode
|
||||
.settings
|
||||
/*.egg-info/
|
||||
/*.egg/
|
||||
develop-eggs
|
||||
downloads
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
/dist
|
||||
*~
|
||||
distribute-*.tar.gz
|
||||
include/
|
||||
lib/
|
||||
man/
|
||||
share/
|
||||
!.editorconfig
|
||||
!.jscsrc
|
||||
!.jshintrc
|
||||
cms/static/cms/compass_app_log.txt
|
||||
!cms/static/cms/js/dist
|
||||
cms/tests/frontend/coverage
|
||||
node_modules
|
||||
screenshots
|
||||
.idea
|
||||
.better_test.db
|
||||
@@ -1,15 +0,0 @@
|
||||
doc-warnings: yes
|
||||
test-warnings: no
|
||||
strictness: veryhigh
|
||||
max-line-length: 120
|
||||
uses:
|
||||
- django
|
||||
autodetect: yes
|
||||
ignore-paths:
|
||||
- docs
|
||||
- cms/migrations
|
||||
- cms/migrations_django
|
||||
- cms/tests/*
|
||||
- cms/test_utils/*
|
||||
- menus/migrations/*
|
||||
- menus/migrations_django/*
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- 3.6
|
||||
- 3.5
|
||||
- 3.4
|
||||
- 2.7
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- enchant
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- $HOME/.pip/cache
|
||||
|
||||
|
||||
env:
|
||||
global:
|
||||
# coveralls
|
||||
- secure: GzxbqfktWQkf6QQvz0OMk4zHbGI8QWcThTLUzEtktheInxwivOHnoJ1kQu2jVVrk0ZVPbOedE+Cn3QMfi6Wj+y6CREwIhfyzqZV+BIgYu/MpW7vT1BQGiN2suHjFOt/TJ90G41DlBRDc7FxGLqL1Mq8hsEdE0W+/Yszo3aMbp2w=
|
||||
# sauce labs username
|
||||
- secure: RotktnZ0AqeTDYfh2O472pPiolQJ2ZDRPUKGDajYWgnG2n94K7hBd4pnA1H/cd42sOyReC3lmmweWNSbG0DLD9+X+s0fwaqKcWGnYsLAcjOWaNXPMwvvgaivadT34JmS9Wv29zNudPL2A6zNw0CB+YVxUZIA4Cm9984AxbYJGSk=
|
||||
# sauce labs access token
|
||||
- secure: CbPfysSncBB2Ue+VOtLDa8xJvwKl73nJO647zt/9UvZ/3PilnZN9aZv2jHxGvCiFXcez+2AddKptMCcx/5EW5UfRkrWUDHrfLCULU2TfOjmufEGM1eOIXhiAun8WQ85LBzTAYy1r9D514cbU3Yzn3xGZwJljPE8JE4cx3MNN/qQ=
|
||||
# temporary solution until https://github.com/ariya/phantomjs/issues/13953 is resolved
|
||||
- PHANTOMJS_CDNURL=https://s3.amazonaws.com/aldryn-local-assets
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# FRONTEND
|
||||
- python: 3.6
|
||||
env: FRONTEND=1 UNIT=1
|
||||
- python: 3.6
|
||||
env: FRONTEND=1 LINT=1
|
||||
- python: 3.6
|
||||
env: FRONTEND=1 INTEGRATION=1 INTEGRATION_TESTS_BUCKET=1 DJANGO=1.11 DATABASE_URL='sqlite://localhost/testdb.sqlite'
|
||||
- python: 3.6
|
||||
env: FRONTEND=1 INTEGRATION=1 INTEGRATION_TESTS_BUCKET=2 DJANGO=1.11 DATABASE_URL='sqlite://localhost/testdb.sqlite'
|
||||
- python: 3.6
|
||||
env: FRONTEND=1 INTEGRATION=1 INTEGRATION_TESTS_BUCKET=3 DJANGO=1.11 DATABASE_URL='sqlite://localhost/testdb.sqlite'
|
||||
|
||||
# DJANGO 1.11
|
||||
- python: 2.7
|
||||
env: DJANGO=1.11 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
- python: 3.4
|
||||
env: DJANGO=1.11 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
- python: 3.5
|
||||
env: DJANGO=1.11 DATABASE_URL='mysql://root@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=1.11 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=1.11 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='emailuserapp.EmailUser'
|
||||
- python: 3.6
|
||||
env: DJANGO=1.11 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='customuserapp.User'
|
||||
|
||||
# DJANGO 2.0
|
||||
- python: 3.4
|
||||
env: DJANGO=2.0 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
- python: 3.5
|
||||
env: DJANGO=2.0 DATABASE_URL='mysql://root@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.0 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.0 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='emailuserapp.EmailUser'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.0 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='customuserapp.User'
|
||||
|
||||
# DJANGO 2.1
|
||||
- python: 3.5
|
||||
env: DJANGO=2.1 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
- python: 3.5
|
||||
env: DJANGO=2.1 DATABASE_URL='mysql://root@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.1 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.1 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='emailuserapp.EmailUser'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.1 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='customuserapp.User'
|
||||
- python: 3.6
|
||||
env: TEST_DOCS=1 DJANGO=2.1 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
|
||||
# DJANGO 2.2
|
||||
- python: 3.5
|
||||
env: DJANGO=2.2 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- python: 3.5
|
||||
env: DJANGO=2.2 DATABASE_URL='mysql://root@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.2 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.2 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='emailuserapp.EmailUser'
|
||||
- python: 3.6
|
||||
env: DJANGO=2.2 DATABASE_URL='postgres://postgres@127.0.0.1/djangocms_test' AUTH_USER_MODEL='customuserapp.User'
|
||||
- python: 3.6
|
||||
env: TEST_DOCS=1 DJANGO=2.2 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
dist: xenial
|
||||
sudo: true
|
||||
allow_failures:
|
||||
- python: 2.7
|
||||
env: DJANGO=1.11 DATABASE_URL='sqlite://localhost/:memory:'
|
||||
exclude:
|
||||
- python: 2.7
|
||||
- python: 3.4
|
||||
- python: 3.5
|
||||
- python: 3.6
|
||||
fast_finish: true
|
||||
|
||||
before_script:
|
||||
- pip freeze
|
||||
- if [ "$DATABASE_URL" == "postgres://postgres@127.0.0.1/djangocms_test" ]; then psql -c "DROP DATABASE IF EXISTS djangocms_test;" -U postgres; fi
|
||||
- if [ "$DATABASE_URL" == "postgres://postgres@127.0.0.1/djangocms_test" ]; then psql -c "create database djangocms_test;" -U postgres; fi
|
||||
- if [ "$DATABASE_URL" == "mysql://root@127.0.0.1/djangocms_test" ]; then mysql -e 'create database IF NOT EXISTS djangocms_test CHARACTER SET utf8 COLLATE utf8_general_ci;'; fi
|
||||
|
||||
before_install:
|
||||
- "export TRAVIS_COMMIT_MSG=\"$(git log --format=%B --no-merges -n 1)\""
|
||||
- pip install -U pip>=8.0
|
||||
- echo "$TRAVIS_COMMIT_MSG" | grep '\[skip saucelabs\]'; export USE_SAUCE_LABS=$?; true
|
||||
- echo "$TRAVIS_COMMIT_MSG" | grep '\[ci only docs\]'; export ONLY_DOCS=$?; true
|
||||
|
||||
install:
|
||||
- if [ "$UNIT" != 1 ] && [ "$LINT" != 1 ]; then pip install -r "test_requirements/django-$DJANGO.txt"; pip freeze; fi
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then nvm install 6 && nvm use 6; fi
|
||||
# Disable the spinner, it looks bad on Travis
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then npm config set spin false; fi
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then npm install -g gulp@3.9.1; fi
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then scripts/install-npm-dependencies.sh; fi
|
||||
- if [ "$UNIT" != 1 ] && [ "$LINT" != 1 ] && [ "$DATABASE_URL" == 'postgres://postgres@127.0.0.1/djangocms_test' ]; then pip install psycopg2 ; fi
|
||||
- if [ "$UNIT" != 1 ] && [ "$LINT" != 1 ] && [ "$DATABASE_URL" == 'mysql://root@127.0.0.1/djangocms_test' ]; then pip install mysqlclient ; fi
|
||||
|
||||
script:
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$UNIT" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then gulp tests:unit; fi;
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$LINT" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then gulp lint; fi;
|
||||
- if [ "$FRONTEND" == 1 ] && [ "$INTEGRATION" == 1 ] && [ "$ONLY_DOCS" != 0 ]; then pip install -e .; pip freeze; gulp tests:integration; fi;
|
||||
- if [ "$FRONTEND" != 1 ] && ([ "$TEST_DOCS" == 1 ] && [ "$ONLY_DOCS" == 0 ] || [ "$ONLY_DOCS" != 0 ]); then coverage run --parallel-mode manage.py test; fi;
|
||||
- if [ "$FRONTEND" != 1 ] && ([ "$TEST_DOCS" == 1 ] && [ "$ONLY_DOCS" == 0 ] || [ "$ONLY_DOCS" != 0 ]); then coverage combine; fi;
|
||||
|
||||
after_success: if [ "$FRONTEND" != 1 ]; then coveralls; fi;
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
- irc.freenode.org#django-cms
|
||||
- irc.freenode.org#django-cms-sprint
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
[django-cms.js]
|
||||
file_filter = cms/locale/<lang>/LC_MESSAGES/djangojs.po
|
||||
source_file = cms/locale/en/LC_MESSAGES/djangojs.po
|
||||
source_lang = en
|
||||
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[django-cms.core]
|
||||
file_filter = cms/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_file = cms/locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
|
||||
@@ -1,562 +0,0 @@
|
||||
Current or previous core committers:
|
||||
|
||||
* Angelo Dini
|
||||
* Beni Wohlwend
|
||||
* Chris Glass
|
||||
* Daniele Procida
|
||||
* Eric Robitaille
|
||||
* Iacopo Spalletti
|
||||
* Jonas Obrist
|
||||
* Martin Koistinen
|
||||
* Patrick Lauber
|
||||
* Paulo Alvarado
|
||||
* Peter Ciciman
|
||||
* Stefan Foulis
|
||||
* Øyvind Saltvik
|
||||
|
||||
|
||||
Current and previous core designers:
|
||||
|
||||
* Christian Bertschy
|
||||
* Matthias Nüesch
|
||||
|
||||
|
||||
Contributors (based on gitlog, 537 unique authors):
|
||||
|
||||
* Антон Евжаков
|
||||
* A. Bram Neijt
|
||||
* aaloy
|
||||
* Aaron Fay
|
||||
* Aaron Renner
|
||||
* Aaron Spike
|
||||
* aball
|
||||
* Adam Chainz
|
||||
* Adi Sieker
|
||||
* Admin Adminaras
|
||||
* Adrien Brunet
|
||||
* Adrián Ribao Martínez
|
||||
* Aidas Bendoraitis
|
||||
* Alberto Paro
|
||||
* Ales Kocjancic
|
||||
* Ales Zabala Alava (Shagi)
|
||||
* Alessandro Pasotti
|
||||
* Alessandro Ronchi
|
||||
* Alex Cucu
|
||||
* Alex Marandon
|
||||
* Alexander Paramonov
|
||||
* Alexandre Leray
|
||||
* Alexey Subbotin
|
||||
* Aliaksei Urbanski
|
||||
* alskgj
|
||||
* Alvin Mites
|
||||
* Anatoly Ivanov
|
||||
* Andi Albrecht
|
||||
* Andras Gyömrey
|
||||
* Andre Bossard
|
||||
* Andrea Stagi
|
||||
* Andreas Elvers
|
||||
* Andrei Avram
|
||||
* Andrei Fokau
|
||||
* Andrew Schoen
|
||||
* André Cruz
|
||||
* Angelo Dini
|
||||
* angular_circle
|
||||
* Anthony Steinhauser
|
||||
* Antoine Catton
|
||||
* Anton Parkhomenko
|
||||
* AQNOUCH Mohammed
|
||||
* Arcady Usov
|
||||
* Arne Gellhaus
|
||||
* Arne Schauf
|
||||
* Artem Skoretskiy
|
||||
* Arthur Debert
|
||||
* Artis Avotins
|
||||
* Aymeric Augustin
|
||||
* Bartosz Płóciennik
|
||||
* Basile LEGAL
|
||||
* behrooz
|
||||
* Behrooz Nobakht
|
||||
* Belegnar
|
||||
* Ben Northway
|
||||
* Ben Stähli
|
||||
* Benjamin
|
||||
* Benjamin Liles
|
||||
* Benjamin Wohlwend
|
||||
* benjaoming
|
||||
* benzkji
|
||||
* Bernardo Cabezas Serra
|
||||
* Bernd Zeimetz
|
||||
* Bertrand Bordage
|
||||
* beshrkayali
|
||||
* Bitdeli Chef
|
||||
* Bjorn Sandberg
|
||||
* Bob Karreman
|
||||
* Bouke Haarsma
|
||||
* Brad Walker
|
||||
* Braden MacDonald
|
||||
* Brian Schott
|
||||
* brightwhitefox
|
||||
* Calvin Cheng
|
||||
* Carlo Ascani
|
||||
* Carlos de Paula
|
||||
* casio
|
||||
* centralniak
|
||||
* Cezar Pendarovski
|
||||
* cezar77
|
||||
* Chanita Siridechkun
|
||||
* Charlie Denton
|
||||
* Charpentier Johan
|
||||
* Chematronix
|
||||
* Cheng-Chia Tseng
|
||||
* Chris Adams
|
||||
* Chris Franklin
|
||||
* Chris Glass
|
||||
* Chris Hughes
|
||||
* Chris Jones
|
||||
* Chris Wesseling
|
||||
* Chris Wilson
|
||||
* Christian Strappazzon
|
||||
* Christof Hagedorn
|
||||
* Christopher Grebs
|
||||
* Christos Kopanos
|
||||
* cip
|
||||
* Claudio Bartolini
|
||||
* Colin Wirz
|
||||
* Conrado Buhrer
|
||||
* Corey Farwell
|
||||
* Craig de Stigter
|
||||
* Craig Ricciuto
|
||||
* creakings
|
||||
* cworth-gh
|
||||
* Damian Moore
|
||||
* Damir Arbula
|
||||
* DamirArbula
|
||||
* Daniel Barden
|
||||
* daniele
|
||||
* Daniele Procida
|
||||
* Danilo Bargen
|
||||
* DaNmarner
|
||||
* darbula
|
||||
* Darii Denis
|
||||
* Darii Denis Home
|
||||
* Dario Albanesi
|
||||
* Darren Pearce
|
||||
* Darryl Woods
|
||||
* Dave Bell
|
||||
* Dave Hall
|
||||
* David D Lowe
|
||||
* David Grant
|
||||
* David Jean Louis
|
||||
* David Lam
|
||||
* David Lee
|
||||
* David Thompson
|
||||
* Davide Setti
|
||||
* derek73
|
||||
* deshack
|
||||
* digi604
|
||||
* divio
|
||||
* Diógenes Augusto Fernandes Hermínio
|
||||
* dlamotte
|
||||
* Dmitry Akinin
|
||||
* Dmitry Gorelik
|
||||
* Dmytro Gorelik
|
||||
* Donatas
|
||||
* Donatas Kučinskas
|
||||
* Douwe van der Meij
|
||||
* Dries Desmet
|
||||
* dstufft
|
||||
* Dylann CORDEL
|
||||
* DylannCordel
|
||||
* DZ
|
||||
* eged
|
||||
* Egor V. Nazarkin
|
||||
* Ekrem SEREN
|
||||
* Elias Probst
|
||||
* Eraldo Energy
|
||||
* Eric Amador
|
||||
* Eric Eldredge
|
||||
* Eric Robitaille
|
||||
* ericr
|
||||
* Erik Allik
|
||||
* Erlend Dalen
|
||||
* Eugene MechanisM
|
||||
* Evandro Miquelito
|
||||
* evildmp
|
||||
* F. Gabriel Gosselin
|
||||
* f4nt
|
||||
* fcurella
|
||||
* febsn
|
||||
* Filip Kazimierczak
|
||||
* Filipe Waitman
|
||||
* FinalAngel
|
||||
* fivethreeo
|
||||
* fj
|
||||
* floppya
|
||||
* Florian Verdet
|
||||
* Frank
|
||||
* Frank Bieniek
|
||||
* FrankBie
|
||||
* frost-nzcr4
|
||||
* furiousdave
|
||||
* Gabe Jackson
|
||||
* Gabriel Hurley
|
||||
* GaretJax
|
||||
* Gavin Wahl
|
||||
* genti94
|
||||
* Gentian Mazrekaj
|
||||
* gentleShark
|
||||
* Geoffrey Fairchild
|
||||
* Geoffrey Hing
|
||||
* George Marshall
|
||||
* Gerard Świderski
|
||||
* Germano Gabbianelli
|
||||
* Gianluca Guarini
|
||||
* Gilles Lenfant
|
||||
* Gleb Chipiga
|
||||
* Gokmen Gorgen
|
||||
* Gregory Klupar
|
||||
* guandalino
|
||||
* Hamish Downer
|
||||
* Hans Andersen
|
||||
* hedberg
|
||||
* henning
|
||||
* Henning Sprang
|
||||
* heppstux
|
||||
* Herbert Poul
|
||||
* hjkelly
|
||||
* Horst Gutmann
|
||||
* hysia
|
||||
* Iacopo Spalletti
|
||||
* Ian Lewis
|
||||
* ikudryavtsev
|
||||
* indexofire
|
||||
* Ionel Maries Cristian
|
||||
* Ivan Vershigora
|
||||
* Iván Raskovsky
|
||||
* izi
|
||||
* J. Cliff Dyer
|
||||
* Jacob Rief
|
||||
* Jakob Hedman
|
||||
* Jakub Randák
|
||||
* jalaziz
|
||||
* Jameel Al-Aziz
|
||||
* James Richards
|
||||
* James Rutherford
|
||||
* Jana Deutschlaender
|
||||
* Jannik V
|
||||
* Jannis Leidel
|
||||
* Janusz Harkot
|
||||
* Jared Proffitt
|
||||
* Jason Brown
|
||||
* Jason Davies
|
||||
* Jason Jenkins
|
||||
* Jason Robinson
|
||||
* Jeffrey Goettsch
|
||||
* Jens Diemer
|
||||
* JensDiemer
|
||||
* Jeroen
|
||||
* Jeroen Noten
|
||||
* Jessica Tallon
|
||||
* Jimmy Lam
|
||||
* Johannes Bornhold
|
||||
* John Bazik
|
||||
* John-Scott Atlakson
|
||||
* Jon Prindiville
|
||||
* Jonas Obrist
|
||||
* Jonathan Liuti
|
||||
* Jonathan Stoppani
|
||||
* jordanjambazov
|
||||
* Jorge Vargas
|
||||
* Jos van Velzen
|
||||
* Joseph Bergantine
|
||||
* Joseph Lin
|
||||
* Joseph Melettukunnel
|
||||
* Josh Kalderimis
|
||||
* Josh Schneier
|
||||
* João Luiz Lorencetti
|
||||
* Julien Poissonnier
|
||||
* Julz
|
||||
* Júlio R. Lucchese
|
||||
* Kalle Bronsen
|
||||
* kater169
|
||||
* Katie McLaughlin
|
||||
* kblomqvist
|
||||
* Kegan Holtzhausen
|
||||
* Keryn Knight
|
||||
* Kevin Burton
|
||||
* Kevin Funk
|
||||
* Kevin Richardson
|
||||
* Kim Thoenen
|
||||
* kochin
|
||||
* Kristian Øllegaard
|
||||
* Krzysztof Socha
|
||||
* kunitoki
|
||||
* L-A Iscla
|
||||
* Lars Smit
|
||||
* Lars van de Kerkhof
|
||||
* Laurens Bosscher
|
||||
* Leon Smith
|
||||
* Leonardo
|
||||
* Lio Mendonca
|
||||
* Lionardo Mendonça
|
||||
* littlepea
|
||||
* Loriana Indelicato
|
||||
* lovmat
|
||||
* Lucas Vogelsang
|
||||
* Lucio Asnaghi
|
||||
* Ludwig Hähne
|
||||
* lug
|
||||
* Luis Diego Garcia
|
||||
* Luke Crooks
|
||||
* Luke Plant
|
||||
* m000
|
||||
* Maik Lustenberger
|
||||
* maiklust
|
||||
* Manolis Stamatogiannakis
|
||||
* Manuel Schmidt
|
||||
* Marc-Olivier Titeux
|
||||
* Marco Badan
|
||||
* Marco Bonetti
|
||||
* Marco Federighi
|
||||
* Marco Paolini
|
||||
* Marco Rimoldi
|
||||
* marcor
|
||||
* Marijn Goedegebure
|
||||
* Mark
|
||||
* Mark Rogers
|
||||
* Mark Walker
|
||||
* Markus Holtermann
|
||||
* Markus Zapke-Gründemann
|
||||
* Marti Raudsepp
|
||||
* Martin
|
||||
* Martin Bambas
|
||||
* Martin Brochhaus
|
||||
* Martin Koistinen
|
||||
* Martin Kosír
|
||||
* Martin Owens
|
||||
* Martin Pajuste
|
||||
* martinkosir
|
||||
* Matas Dailyda
|
||||
* Mateusz Dereniowski
|
||||
* Mateusz Kamycki
|
||||
* Mateusz Marzantowicz
|
||||
* mathijs
|
||||
* Matt Chisholm
|
||||
* Matteo Rosati
|
||||
* Matthias Cavigelli
|
||||
* Mattia Larentis
|
||||
* Max Shkurygin
|
||||
* Maxim Bodyansky
|
||||
* Maxime Haineault
|
||||
* mbouchar
|
||||
* mcosta
|
||||
* meers
|
||||
* Mel Collins
|
||||
* meomap
|
||||
* MerLex
|
||||
* Michael P. Jung
|
||||
* Michael Thornhill
|
||||
* Michal Danilak
|
||||
* Mike Dory
|
||||
* Mike Johnson
|
||||
* mikek
|
||||
* Mikko Ahlroth
|
||||
* Milo Price
|
||||
* Mitar
|
||||
* Mokys
|
||||
* Morgan Wahl
|
||||
* Motiejus Jakštys
|
||||
* motleytech
|
||||
* mrlundis
|
||||
* mvaerle
|
||||
* MW
|
||||
* Mykhailo Kolesnyk
|
||||
* Nelson Brochado
|
||||
* neoprolog
|
||||
* Nick Jones
|
||||
* Nicolas
|
||||
* Nicolas PASCAL
|
||||
* nikolas
|
||||
* Nina Zakharenko
|
||||
* nsh
|
||||
* o-zander
|
||||
* ojii
|
||||
* Oliver Thane
|
||||
* Oliver Zander
|
||||
* Olivier Larchevêque
|
||||
* Olivier Le Brouster
|
||||
* Orne Brocaar
|
||||
* Oyvind Saltvik
|
||||
* padelt
|
||||
* Pankrat
|
||||
* Paolo Leggio
|
||||
* Paolo Romolini
|
||||
* Pascal Mouret
|
||||
* pascal.beyeler
|
||||
* Patrick Arminio
|
||||
* Patrick Lauber
|
||||
* Patrick Toal
|
||||
* patricklauber
|
||||
* Patryk Zawadzki
|
||||
* paul
|
||||
* Paul van der Linden
|
||||
* Paulo
|
||||
* Paulo Alvarado
|
||||
* Pavel Puchkin
|
||||
* Pawel Markowski
|
||||
* pbgc
|
||||
* pcicman
|
||||
* Pedro Gracia
|
||||
* Pete Loggie
|
||||
* Peter Farrell
|
||||
* Peter J. Farrell
|
||||
* Peter Landry
|
||||
* Peter-Paul van Gemerden
|
||||
* peterfarrell
|
||||
* Petteri
|
||||
* pgcd
|
||||
* Philipp Bosch
|
||||
* Philipp Zedler
|
||||
* philomat
|
||||
* phuihock
|
||||
* Pigletto
|
||||
* Piotr Jakimiak
|
||||
* Piotr Kilczuk
|
||||
* piquadrat
|
||||
* pumalo
|
||||
* Rafal Radulski
|
||||
* raidsan
|
||||
* Rainer Koirikivi
|
||||
* Rajesh
|
||||
* Rajesh K.
|
||||
* Rajesh P.
|
||||
* Rebecca Breu
|
||||
* Remco Wendt
|
||||
* René Fleschenberg
|
||||
* requires.io
|
||||
* Restless Being
|
||||
* Richard Barran
|
||||
* rizumu
|
||||
* Robert Barsch
|
||||
* Robert Buchholz
|
||||
* Robert Clark
|
||||
* Robert Feldbinder
|
||||
* Robert Pogorzelski
|
||||
* Robert Stein
|
||||
* Roberto
|
||||
* Roberto Bampi
|
||||
* Robin Lewis
|
||||
* robint
|
||||
* Rodolfo Carvalho
|
||||
* Roman Kozlovskiy
|
||||
* root
|
||||
* rtpm
|
||||
* Ruslan Malogulko
|
||||
* Russ Ferriday
|
||||
* SachaMPS
|
||||
* Salmanul Farzy
|
||||
* Sam Manzi
|
||||
* Samuel Lahti
|
||||
* Samuel Luescher
|
||||
* Samuel Lüscher
|
||||
* sbussetti
|
||||
* Schoen
|
||||
* scottbarnham
|
||||
* sealibora
|
||||
* Sean Bleier
|
||||
* Sebastian Braun
|
||||
* Sergey Fedoseev
|
||||
* Seth Buntin
|
||||
* Seyhun Akyurek
|
||||
* Shahar Or
|
||||
* Shatalov Vadim
|
||||
* Shaun Brady
|
||||
* shed
|
||||
* Shinya Okano
|
||||
* shulcsm
|
||||
* Simon Charette
|
||||
* Simon Hedberg
|
||||
* Simon Meers
|
||||
* sleytr
|
||||
* Sophie Leroy
|
||||
* spookylukey
|
||||
* srj55
|
||||
* ssteinerX
|
||||
* Stavros Korokithakis
|
||||
* Stefan Foulis
|
||||
* Stefan Wehrmeyer
|
||||
* stefanfoulis
|
||||
* Stefano Brentegani
|
||||
* Stefano Crosta
|
||||
* Stefano Morandi
|
||||
* Stefano Probst
|
||||
* Steffen Jasper
|
||||
* Stephan
|
||||
* Stephan Hepper
|
||||
* Stephan Herzog
|
||||
* Stephan Jaekel
|
||||
* Stephen Muss
|
||||
* Stephen Paulger
|
||||
* Stephen Watkin
|
||||
* Steve Steiner
|
||||
* Steven Laroche
|
||||
* Sylvain Fankhauser
|
||||
* Tadas Dailyda
|
||||
* Tanel Külaots
|
||||
* tdelam
|
||||
* tehfink
|
||||
* Thibaud Colas
|
||||
* Thomas Meitz
|
||||
* Thomas Parslow
|
||||
* Thomas Remmert
|
||||
* Thomas Woolford
|
||||
* thomas-d
|
||||
* Tim Anderegg
|
||||
* Tim Davies
|
||||
* Tim Graham
|
||||
* timesong
|
||||
* Tino de Bruijn
|
||||
* tiret
|
||||
* Tobias von Klipstein
|
||||
* Tom
|
||||
* Tom Berger
|
||||
* Tom de Simone
|
||||
* Tom S
|
||||
* Tom V
|
||||
* Tom Wardill
|
||||
* Ulrich Petri
|
||||
* unknown
|
||||
* uno
|
||||
* User
|
||||
* Vadim Lopatyuk
|
||||
* Vadim Sikora
|
||||
* Venelin Stoykov
|
||||
* Viktor Nagy
|
||||
* Viliam Segeda
|
||||
* Vinit (Vermicelli on IRC)
|
||||
* vinit kumar
|
||||
* Vinod Kurup
|
||||
* Vladimir Bolshakov
|
||||
* Vladyslav
|
||||
* vvangelovski
|
||||
* wangJunjie
|
||||
* Wayne Moore
|
||||
* wid
|
||||
* wildermesser
|
||||
* WYSIATI
|
||||
* Xavier Fernandez
|
||||
* Xavier Ordoquy
|
||||
* xie wei
|
||||
* xray7224
|
||||
* Yann Malet
|
||||
* yann.malet@gmail.com
|
||||
* ychouinard
|
||||
* yedpodtrzitko
|
||||
* yohanboniface
|
||||
* Yosuke Ikeda
|
||||
* Yuri van der Meer
|
||||
* zandeez
|
||||
* zundoya
|
||||
* Žan Anderle
|
||||
* Óscar M. Lage
|
||||
* Øyvind Saltvik
|
||||
-1107
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
||||
Copyright (c) 2008-present, Batiste Bieler
|
||||
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 the author nor the names of other
|
||||
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.
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
include AUTHORS
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include CHANGELOG.txt
|
||||
recursive-include cms/locale *
|
||||
recursive-include cms/templates *
|
||||
recursive-include cms/static *
|
||||
recursive-include cms/plugins *
|
||||
recursive-include menus/templates *
|
||||
recursive-include stacks/templates *
|
||||
recursive-include docs *
|
||||
recursive-exclude * *.pyc
|
||||
recursive-exclude * *.scssc
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
##########
|
||||
django CMS
|
||||
##########
|
||||
.. image:: https://travis-ci.org/divio/django-cms.svg?branch=develop
|
||||
:target: https://travis-ci.org/divio/django-cms
|
||||
.. image:: https://img.shields.io/pypi/v/django-cms.svg
|
||||
:target: https://pypi.python.org/pypi/django-cms/
|
||||
.. image:: https://img.shields.io/badge/wheel-yes-green.svg
|
||||
:target: https://pypi.python.org/pypi/django-cms/
|
||||
.. image:: https://img.shields.io/pypi/l/django-cms.svg
|
||||
:target: https://pypi.python.org/pypi/django-cms/
|
||||
.. image:: https://codeclimate.com/github/divio/django-cms/badges/gpa.svg
|
||||
:target: https://codeclimate.com/github/divio/django-cms
|
||||
:alt: Code Climate
|
||||
|
||||
Open source enterprise content management system based on the Django framework.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/divio/django-cms/develop/docs/images/try-with-divio.png
|
||||
:target: http://try.django-cms.org/
|
||||
:alt: Try demo with Divio Cloud
|
||||
|
||||
.. ATTENTION::
|
||||
|
||||
To propose **significant new features**, open pull requests based on and made against the **develop** branch. It's
|
||||
the branch for features that will go into the next django CMS feature release.
|
||||
|
||||
To propose **fixes and backwards-compatible improvements**, please work on the latest **release** branch. This is
|
||||
the branch that will become the next PyPI release ("the next version of django CMS").
|
||||
|
||||
Security fixes will be backported to older branches by the core team as appropriate.
|
||||
|
||||
|
||||
********
|
||||
Features
|
||||
********
|
||||
|
||||
* hierarchical pages
|
||||
* extensive built-in support for multilingual websites
|
||||
* multi-site support
|
||||
* draft/publish workflows
|
||||
* version control
|
||||
* a sophisticated publishing architecture, that's also usable in your own applications
|
||||
* frontend content editing
|
||||
* a hierarchical content structure for nested plugins
|
||||
* an extensible navigation system that your own applications can hook into
|
||||
* SEO-friendly URLs
|
||||
* designed to integrate thoroughly into other applications
|
||||
|
||||
Developing applications that integrate with and take advantage of django CMS features is easy and well-documented.
|
||||
|
||||
More information on `our website <https://www.django-cms.org>`_.
|
||||
|
||||
************
|
||||
Requirements
|
||||
************
|
||||
|
||||
See the `Python/Django requirements for the current release version
|
||||
<http://docs.django-cms.org/en/latest/#software-version-requirements-and-release-notes>`_ in our documentation.
|
||||
|
||||
See the `installation how-to guide for an overview of some other requirements and dependencies of the current release
|
||||
<http://docs.django-cms.org/en/latest/how_to/install.html>`_
|
||||
|
||||
*************
|
||||
Documentation
|
||||
*************
|
||||
|
||||
We maintain documentation for several versions of the project. Key versions are:
|
||||
|
||||
* `stable <http://docs.django-cms.org>`_ (default), for the **current release** version
|
||||
* `latest <http://docs.django-cms.org/en/latest/>`_, representing the latest build of the **release-3.4.x branch**
|
||||
* `develop <http://docs.django-cms.org/en/develop/>`_, representing the latest build of the **develop branch**
|
||||
|
||||
For more information about our branch policy, see `Branches
|
||||
<http://docs.django-cms.org/en/latest/contributing/development-policies.html>`_.
|
||||
|
||||
Our documentation is hosted courtesy of `Read the Docs <https://readthedocs.org>`_.
|
||||
|
||||
|
||||
********
|
||||
Tutorial
|
||||
********
|
||||
|
||||
http://docs.django-cms.org/en/latest/introduction/index.html
|
||||
|
||||
***********
|
||||
Quick Start
|
||||
***********
|
||||
|
||||
You can use the `django CMS installer <https://djangocms-installer.readthedocs.io>`_::
|
||||
|
||||
$ pip install --upgrade virtualenv
|
||||
$ virtualenv env
|
||||
$ source env/bin/activate
|
||||
(env) $ pip install djangocms-installer
|
||||
(env) $ mkdir myproject && cd myproject
|
||||
(env) $ djangocms -f -p . my_demo
|
||||
(env) $ python manage.py
|
||||
|
||||
|
||||
************
|
||||
Getting Help
|
||||
************
|
||||
|
||||
Please head over to our IRC channel, #django-cms, on irc.freenode.net or write
|
||||
to our `mailing list <https://groups.google.com/forum/#!forum/django-cms>`_.
|
||||
|
||||
If you don't have an IRC client, you can `join our IRC channel using the KiwiIRC web client
|
||||
<https://kiwiirc.com/client/irc.freenode.net/django-cms>`_, which works pretty well.
|
||||
|
||||
******************
|
||||
Commercial support
|
||||
******************
|
||||
|
||||
This project is backed by `Divio <https://www.divio.com/en/commercial-support/>`_.
|
||||
If you need help implementing or hosting django CMS, please contact us:
|
||||
sales@divio.com.
|
||||
|
||||
*******
|
||||
Credits
|
||||
*******
|
||||
|
||||
* Includes icons from `FamFamFam <http://www.famfamfam.com>`_.
|
||||
* Python tree engine powered by
|
||||
`django-treebeard <https://tabo.pe/projects/django-treebeard/>`_.
|
||||
* JavaScript tree in admin uses `jsTree <https://www.jstree.com>`_.
|
||||
* Many thanks to the
|
||||
`over 515 contributors <https://github.com/divio/django-cms/blob/develop/AUTHORS>`_
|
||||
to the django CMS!
|
||||
@@ -1,32 +0,0 @@
|
||||
- Put beer in fridge for cooling (see last step)
|
||||
- Create a release branch (locally)
|
||||
- Bump the version in cms/__init__.py
|
||||
- Build newest english translation (django-admin.py makemessages -l en)
|
||||
- Build newest english JS translations (django-admin.py makemessages -l en -d djangojs)
|
||||
- Pull translations (tx pull -f -a)
|
||||
- Compile translations (django-admin.py compilemessages)
|
||||
- Make sure icons are recreated in the new place (gulp icons)
|
||||
- Make sure CSS files are compiled from latest sass source (gulp sass)
|
||||
- Make sure JS files are bundled and minified propery from latest source (gulp bundle)
|
||||
- Make sure to remove JS, CSS and font files from the older version of CMS
|
||||
- Check CHANGELOG that all closed PRs are present.
|
||||
- Prepare blog post, including "Release" tag
|
||||
- update AUTHORS file (python develop.py authors)
|
||||
- Make a release commit
|
||||
- Tag release commit
|
||||
- Merge back into develop
|
||||
- Push develop to GitHub
|
||||
- Merge into master
|
||||
- Push master to GitHub (WITH --tags)
|
||||
- Release to PyPI (ONLY FOR FINAL VERSIONS!) (python setup.py sdist upload)
|
||||
- Release wheel (python setup.py bdist_wheel upload)
|
||||
- Bump version in develop branch to .dev1
|
||||
- Publish blog post
|
||||
- Tweet blog post
|
||||
- Post notice on django-users, django-cms and django-cms-developers mailing lists
|
||||
- Update IRC channel topic (/topic this is the new topic)
|
||||
- Add current version to RTD versions.
|
||||
- Set new version as default version on RTD
|
||||
- Make sure tutorials on the website still work
|
||||
- Make sure download button on homepage (website) is updated
|
||||
- Have a beer! (If anyone else is around, do a release party!)
|
||||
@@ -1,2 +0,0 @@
|
||||
last 2 versions
|
||||
> 1%
|
||||
@@ -1,5 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '3.6.0'
|
||||
|
||||
default_app_config = 'cms.apps.CMSConfig'
|
||||
@@ -1,11 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import cms.admin.pageadmin
|
||||
import cms.admin.useradmin
|
||||
import cms.admin.permissionadmin
|
||||
import cms.admin.settingsadmin
|
||||
import cms.admin.static_placeholder # nopyflakes
|
||||
# Piggyback off admin.autodiscover() to discover cms plugins
|
||||
from cms import plugin_pool
|
||||
|
||||
|
||||
plugin_pool.plugin_pool.discover_plugins()
|
||||
-1356
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,182 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from copy import deepcopy
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import site
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db import OperationalError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from cms.admin.forms import GlobalPagePermissionAdminForm, PagePermissionInlineAdminForm, ViewRestrictionInlineAdminForm
|
||||
from cms.exceptions import NoPermissionsException
|
||||
from cms.models import PagePermission, GlobalPagePermission
|
||||
from cms.utils import permissions, page_permissions
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.helpers import classproperty
|
||||
|
||||
|
||||
PERMISSION_ADMIN_INLINES = []
|
||||
|
||||
user_model = get_user_model()
|
||||
admin_class = UserAdmin
|
||||
for model, admin_instance in site._registry.items():
|
||||
if model == user_model:
|
||||
admin_class = admin_instance.__class__
|
||||
|
||||
|
||||
class TabularInline(admin.TabularInline):
|
||||
pass
|
||||
|
||||
|
||||
class PagePermissionInlineAdmin(TabularInline):
|
||||
model = PagePermission
|
||||
# use special form, so we can override of user and group field
|
||||
form = PagePermissionInlineAdminForm
|
||||
classes = ['collapse', 'collapsed']
|
||||
extra = 0 # edit page load time boost
|
||||
show_with_view_permissions = False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if not obj:
|
||||
return False
|
||||
return page_permissions.user_can_change_page_permissions(
|
||||
request.user,
|
||||
page=obj,
|
||||
site=obj.node.site,
|
||||
)
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return self.has_change_permission(request, obj)
|
||||
|
||||
@classproperty
|
||||
def raw_id_fields(cls):
|
||||
# Dynamically set raw_id_fields based on settings
|
||||
threshold = get_cms_setting('RAW_ID_USERS')
|
||||
|
||||
# Given a fresh django-cms install and a django settings with the
|
||||
# CMS_RAW_ID_USERS = CMS_PERMISSION = True
|
||||
# django throws an OperationalError when running
|
||||
# ./manage migrate
|
||||
# because auth_user doesn't exists yet
|
||||
try:
|
||||
threshold = threshold and get_user_model().objects.count() > threshold
|
||||
except OperationalError:
|
||||
threshold = False
|
||||
|
||||
return ['user'] if threshold else []
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""
|
||||
Queryset change, so user with global change permissions can see
|
||||
all permissions. Otherwise user can see only permissions for
|
||||
peoples which are under him (he can't see his permissions, because
|
||||
this will lead to violation, when he can add more power to himself)
|
||||
"""
|
||||
site = Site.objects.get_current(request)
|
||||
|
||||
try:
|
||||
# can see only permissions for users which are under him in tree
|
||||
qs = self.model.objects.subordinate_to_user(request.user, site)
|
||||
except NoPermissionsException:
|
||||
return self.model.objects.none()
|
||||
return qs.filter(can_view=self.show_with_view_permissions)
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
"""
|
||||
Some fields may be excluded here. User can change only
|
||||
permissions which are available for him. E.g. if user does not haves
|
||||
can_publish flag, he can't change assign can_publish permissions.
|
||||
"""
|
||||
exclude = self.exclude or []
|
||||
if obj:
|
||||
user = request.user
|
||||
if not obj.has_add_permission(user):
|
||||
exclude.append('can_add')
|
||||
if not obj.has_delete_permission(user):
|
||||
exclude.append('can_delete')
|
||||
if not obj.has_publish_permission(user):
|
||||
exclude.append('can_publish')
|
||||
if not obj.has_advanced_settings_permission(user):
|
||||
exclude.append('can_change_advanced_settings')
|
||||
if not obj.has_move_page_permission(user):
|
||||
exclude.append('can_move_page')
|
||||
|
||||
kwargs['exclude'] = exclude
|
||||
formset_cls = super(PagePermissionInlineAdmin, self).get_formset(request, obj=obj, **kwargs)
|
||||
qs = self.get_queryset(request)
|
||||
if obj is not None:
|
||||
qs = qs.filter(page=obj)
|
||||
formset_cls._queryset = qs
|
||||
return formset_cls
|
||||
|
||||
|
||||
class ViewRestrictionInlineAdmin(PagePermissionInlineAdmin):
|
||||
extra = 0 # edit page load time boost
|
||||
form = ViewRestrictionInlineAdminForm
|
||||
verbose_name = _("View restriction")
|
||||
verbose_name_plural = _("View restrictions")
|
||||
show_with_view_permissions = True
|
||||
|
||||
|
||||
class GlobalPagePermissionAdmin(admin.ModelAdmin):
|
||||
list_display = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions']
|
||||
list_filter = ['user', 'group', 'can_change', 'can_delete', 'can_publish', 'can_change_permissions']
|
||||
|
||||
form = GlobalPagePermissionAdminForm
|
||||
search_fields = []
|
||||
for field in admin_class.search_fields:
|
||||
search_fields.append("user__%s" % field)
|
||||
search_fields.append('group__name')
|
||||
|
||||
list_display.append('can_change_advanced_settings')
|
||||
list_filter.append('can_change_advanced_settings')
|
||||
|
||||
def get_list_filter(self, request):
|
||||
threshold = get_cms_setting('RAW_ID_USERS')
|
||||
try:
|
||||
threshold = threshold and get_user_model().objects.count() > threshold
|
||||
except OperationalError:
|
||||
threshold = False
|
||||
filter_copy = deepcopy(self.list_filter)
|
||||
if threshold:
|
||||
filter_copy.remove('user')
|
||||
return filter_copy
|
||||
|
||||
def has_add_permission(self, request):
|
||||
site = Site.objects.get_current(request)
|
||||
return permissions.user_can_add_global_permissions(request.user, site)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
site = Site.objects.get_current(request)
|
||||
return permissions.user_can_change_global_permissions(request.user, site)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
site = Site.objects.get_current(request)
|
||||
return permissions.user_can_delete_global_permissions(request.user, site)
|
||||
|
||||
@classproperty
|
||||
def raw_id_fields(cls):
|
||||
# Dynamically set raw_id_fields based on settings
|
||||
threshold = get_cms_setting('RAW_ID_USERS')
|
||||
|
||||
# Given a fresh django-cms install and a django settings with the
|
||||
# CMS_RAW_ID_USERS = CMS_PERMISSION = True
|
||||
# django throws an OperationalError when running
|
||||
# ./manage migrate
|
||||
# because auth_user doesn't exists yet
|
||||
try:
|
||||
threshold = threshold and get_user_model().objects.count() > threshold
|
||||
except OperationalError:
|
||||
threshold = False
|
||||
|
||||
return ['user'] if threshold else []
|
||||
|
||||
|
||||
if get_cms_setting('PERMISSION'):
|
||||
admin.site.register(GlobalPagePermission, GlobalPagePermissionAdmin)
|
||||
PERMISSION_ADMIN_INLINES.extend([
|
||||
ViewRestrictionInlineAdmin,
|
||||
PagePermissionInlineAdmin,
|
||||
])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,139 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from functools import update_wrapper
|
||||
import copy
|
||||
import json
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import ModelAdmin
|
||||
from django.contrib.auth.admin import csrf_protect_m
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseBadRequest
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import override
|
||||
from django.utils.six.moves.urllib.parse import urlparse
|
||||
|
||||
from cms.admin.forms import RequestToolbarForm
|
||||
from cms.models import UserSettings
|
||||
from cms.toolbar.toolbar import CMSToolbar
|
||||
from cms.utils.page import get_page_from_request
|
||||
from cms.utils.urlutils import admin_reverse
|
||||
|
||||
|
||||
class SettingsAdmin(ModelAdmin):
|
||||
|
||||
def get_urls(self):
|
||||
def wrap(view):
|
||||
def wrapper(*args, **kwargs):
|
||||
return self.admin_site.admin_view(view)(*args, **kwargs)
|
||||
|
||||
return update_wrapper(wrapper, view)
|
||||
|
||||
info = self.model._meta.app_label, self.model._meta.model_name
|
||||
|
||||
return [
|
||||
url(r'^session_store/$',
|
||||
self.session_store,
|
||||
name='%s_%s_session_store' % info),
|
||||
url(r'^cms-toolbar/$',
|
||||
wrap(self.get_toolbar),
|
||||
name='%s_%s_get_toolbar' % info),
|
||||
url(r'^$',
|
||||
wrap(self.change_view),
|
||||
name='%s_%s_change' % info),
|
||||
url(r'^(.+)/$',
|
||||
wrap(self.change_view),
|
||||
name='%s_%s_change' % info),
|
||||
]
|
||||
|
||||
@csrf_protect_m
|
||||
@transaction.atomic
|
||||
def change_view(self, request, id=None):
|
||||
model = self.model
|
||||
try:
|
||||
obj = model.objects.get(user=request.user)
|
||||
except model.DoesNotExist:
|
||||
return self.add_view(request)
|
||||
return super(SettingsAdmin, self).change_view(request, str(obj.pk))
|
||||
|
||||
def session_store(self, request):
|
||||
"""
|
||||
either POST or GET
|
||||
POST should have a settings parameter
|
||||
"""
|
||||
if not request.user.is_staff:
|
||||
return HttpResponse(json.dumps(""),
|
||||
content_type="application/json")
|
||||
if request.method == "POST":
|
||||
request.session['cms_settings'] = request.POST['settings']
|
||||
request.session.save()
|
||||
return HttpResponse(
|
||||
json.dumps(request.session.get('cms_settings', '')),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
def get_toolbar(self, request):
|
||||
form = RequestToolbarForm(request.GET or None)
|
||||
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest('Invalid parameters')
|
||||
|
||||
form_data = form.cleaned_data
|
||||
cms_path = form_data.get('cms_path') or request.path_info
|
||||
origin_url = urlparse(cms_path)
|
||||
attached_obj = form_data.get('attached_obj')
|
||||
current_page = get_page_from_request(request, use_path=origin_url.path, clean_path=True)
|
||||
|
||||
if attached_obj and current_page and not (attached_obj == current_page):
|
||||
return HttpResponseBadRequest('Generic object does not match current page')
|
||||
|
||||
data = QueryDict(query_string=origin_url.query, mutable=True)
|
||||
placeholders = request.GET.getlist("placeholders[]")
|
||||
|
||||
if placeholders:
|
||||
data.setlist('placeholders[]', placeholders)
|
||||
|
||||
request = copy.copy(request)
|
||||
request.GET = data
|
||||
request.current_page = current_page
|
||||
request.toolbar = CMSToolbar(request, request_path=origin_url.path, _async=True)
|
||||
request.toolbar.set_object(attached_obj or current_page)
|
||||
return HttpResponse(request.toolbar.render())
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
obj.user = request.user
|
||||
obj.save()
|
||||
|
||||
def response_post_save_change(self, request, obj):
|
||||
#
|
||||
# When the user changes his language setting, we need to do two things:
|
||||
# 1. Change the language-prefix for the sideframed admin view
|
||||
# 2. Reload the whole window so that the new language affects the
|
||||
# toolbar, etc.
|
||||
#
|
||||
# To do this, we first redirect the sideframe to the correct new, URL,
|
||||
# but we pass a GET param 'reload_window', which instructs JS on that
|
||||
# page to strip (to avoid infinite redirection loops) that param then
|
||||
# reload the whole window again.
|
||||
#
|
||||
with override(obj.language):
|
||||
post_url = admin_reverse(
|
||||
'cms_usersettings_change',
|
||||
args=[obj.id, ],
|
||||
current_app=self.admin_site.name
|
||||
)
|
||||
return HttpResponseRedirect("{0}?reload_window".format(post_url))
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
if obj and obj.user == request.user:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_model_perms(self, request):
|
||||
"""
|
||||
Return empty perms dict thus hiding the model from admin index.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
admin.site.register(UserSettings, SettingsAdmin)
|
||||
@@ -1,12 +0,0 @@
|
||||
from cms.models import StaticPlaceholder
|
||||
from django.contrib import admin
|
||||
from cms.admin.placeholderadmin import PlaceholderAdminMixin
|
||||
|
||||
|
||||
class StaticPlaceholderAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
|
||||
list_display = ('get_name', 'code', 'site', 'creation_method')
|
||||
search_fields = ('name', 'code',)
|
||||
exclude = ('creation_method',)
|
||||
list_filter = ('creation_method', 'site')
|
||||
|
||||
admin.site.register(StaticPlaceholder, StaticPlaceholderAdmin)
|
||||
@@ -1,150 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from copy import deepcopy
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import site
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.sites.models import Site
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from cms.admin.forms import PageUserChangeForm, PageUserGroupForm
|
||||
from cms.exceptions import NoPermissionsException
|
||||
from cms.models import Page, PagePermission, PageUser, PageUserGroup
|
||||
from cms.utils.compat.forms import UserAdmin
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.permissions import (
|
||||
get_model_permission_codename,
|
||||
get_subordinate_groups,
|
||||
get_subordinate_users,
|
||||
get_user_permission_level,
|
||||
)
|
||||
|
||||
|
||||
user_model = get_user_model()
|
||||
admin_class = UserAdmin
|
||||
for model, admin_instance in site._registry.items():
|
||||
if model == user_model:
|
||||
admin_class = admin_instance.__class__
|
||||
|
||||
|
||||
class GenericCmsPermissionAdmin(object):
|
||||
|
||||
def get_subordinates(self, user, site):
|
||||
raise NotImplementedError
|
||||
|
||||
def _has_change_permissions_permission(self, request):
|
||||
"""
|
||||
User is able to add/change objects only if he haves can change
|
||||
permission on some page.
|
||||
"""
|
||||
site = Site.objects.get_current(request)
|
||||
|
||||
try:
|
||||
get_user_permission_level(request.user, site)
|
||||
except NoPermissionsException:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form_class = super(GenericCmsPermissionAdmin, self).get_form(request, obj, **kwargs)
|
||||
form_class._current_user = request.user
|
||||
return form_class
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = super(GenericCmsPermissionAdmin, self).get_queryset(request)
|
||||
site = Site.objects.get_current(request)
|
||||
user_ids = self.get_subordinates(request.user, site).values_list('pk', flat=True)
|
||||
return queryset.filter(pk__in=user_ids)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
has_model_perm = super(GenericCmsPermissionAdmin, self).has_add_permission(request)
|
||||
|
||||
if not has_model_perm:
|
||||
return False
|
||||
return self._has_change_permissions_permission(request)
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
has_model_perm = super(GenericCmsPermissionAdmin, self).has_change_permission(request, obj)
|
||||
|
||||
if not has_model_perm:
|
||||
return False
|
||||
return self._has_change_permissions_permission(request)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
has_model_perm = super(GenericCmsPermissionAdmin, self).has_delete_permission(request, obj)
|
||||
|
||||
if not has_model_perm:
|
||||
return False
|
||||
return self._has_change_permissions_permission(request)
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
# For django 2.1
|
||||
# Default is to return True if user got `change` perm, but we have to
|
||||
# get in consideration also cms permission system
|
||||
return self.has_change_permission(request, obj)
|
||||
|
||||
|
||||
class PageUserAdmin(GenericCmsPermissionAdmin, admin_class):
|
||||
form = PageUserChangeForm
|
||||
model = PageUser
|
||||
|
||||
def get_subordinates(self, user, site):
|
||||
return get_subordinate_users(user, site).values_list('pk', flat=True)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = super(PageUserAdmin, self).get_readonly_fields(request, obj)
|
||||
|
||||
if not request.user.is_superuser:
|
||||
# Non superusers can't set superuser status on
|
||||
# their subordinates.
|
||||
fields = list(fields) + ['is_superuser']
|
||||
return fields
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
# By default set the staff flag to True
|
||||
# when a PageUser is first created
|
||||
obj.is_staff = True
|
||||
# Set the created_by field to the current user
|
||||
obj.created_by = request.user
|
||||
super(PageUserAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
|
||||
class PageUserGroupAdmin(GenericCmsPermissionAdmin, admin.ModelAdmin):
|
||||
form = PageUserGroupForm
|
||||
list_display = ('name', 'created_by')
|
||||
|
||||
fieldsets = [
|
||||
(None, {'fields': ('name',)}),
|
||||
]
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
"""
|
||||
Nobody can grant more than he haves, so check for user permissions
|
||||
to Page and User model and render fieldset depending on them.
|
||||
"""
|
||||
fieldsets = deepcopy(self.fieldsets)
|
||||
perm_models = (
|
||||
(Page, ugettext('Page permissions')),
|
||||
(PageUser, ugettext('User & Group permissions')),
|
||||
(PagePermission, ugettext('Page permissions management')),
|
||||
)
|
||||
for i, perm_model in enumerate(perm_models):
|
||||
fields = []
|
||||
model, title = perm_model
|
||||
name = model.__name__.lower()
|
||||
for key in ('add', 'change', 'delete'):
|
||||
perm_code = get_model_permission_codename(model, action=key)
|
||||
if request.user.has_perm(perm_code):
|
||||
fields.append('can_%s_%s' % (key, name))
|
||||
if fields:
|
||||
fieldsets.insert(2 + i, (title, {'fields': (fields,)}))
|
||||
return fieldsets
|
||||
|
||||
def get_subordinates(self, user, site):
|
||||
return get_subordinate_groups(user, site).values_list('pk', flat=True)
|
||||
|
||||
|
||||
if get_cms_setting('PERMISSION'):
|
||||
admin.site.register(PageUser, PageUserAdmin)
|
||||
admin.site.register(PageUserGroup, PageUserGroupAdmin)
|
||||
-532
@@ -1,532 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Public Python API to create CMS contents.
|
||||
|
||||
WARNING: None of the functions defined in this module checks for permissions.
|
||||
You must implement the necessary permission checks in your own code before
|
||||
calling these methods!
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import FieldError
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.template.loader import get_template
|
||||
from django.utils import six
|
||||
from django.utils.translation import activate
|
||||
|
||||
from cms import constants
|
||||
from cms.app_base import CMSApp
|
||||
from cms.apphook_pool import apphook_pool
|
||||
from cms.constants import TEMPLATE_INHERITANCE_MAGIC
|
||||
from cms.models.pagemodel import Page
|
||||
from cms.models.permissionmodels import (PageUser, PagePermission, GlobalPagePermission,
|
||||
ACCESS_PAGE_AND_DESCENDANTS)
|
||||
from cms.models.placeholdermodel import Placeholder
|
||||
from cms.models.pluginmodel import CMSPlugin
|
||||
from cms.models.titlemodels import Title
|
||||
from cms.plugin_base import CMSPluginBase
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils import copy_plugins, get_current_site
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.i18n import get_language_list
|
||||
from cms.utils.page import get_available_slug
|
||||
from cms.utils.permissions import _thread_locals, current_user
|
||||
from menus.menu_pool import menu_pool
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Helpers/Internals
|
||||
#===============================================================================
|
||||
|
||||
|
||||
def _verify_apphook(apphook, namespace):
|
||||
"""
|
||||
Verifies the apphook given is valid and returns the normalized form (name)
|
||||
"""
|
||||
apphook_pool.discover_apps()
|
||||
if isinstance(apphook, CMSApp):
|
||||
try:
|
||||
assert apphook.__class__ in [app.__class__ for app in apphook_pool.apps.values()]
|
||||
except AssertionError:
|
||||
print(apphook_pool.apps.values())
|
||||
raise
|
||||
apphook_name = apphook.__class__.__name__
|
||||
elif hasattr(apphook, '__module__') and issubclass(apphook, CMSApp):
|
||||
return apphook.__name__
|
||||
elif isinstance(apphook, six.string_types):
|
||||
try:
|
||||
assert apphook in apphook_pool.apps
|
||||
except AssertionError:
|
||||
print(apphook_pool.apps.values())
|
||||
raise
|
||||
apphook_name = apphook
|
||||
else:
|
||||
raise TypeError("apphook must be string or CMSApp instance")
|
||||
if apphook_pool.apps[apphook_name].app_name and not namespace:
|
||||
raise ValidationError('apphook with app_name must define a namespace')
|
||||
return apphook_name
|
||||
|
||||
|
||||
def _verify_plugin_type(plugin_type):
|
||||
"""
|
||||
Verifies the given plugin_type is valid and returns a tuple of
|
||||
(plugin_model, plugin_type)
|
||||
"""
|
||||
if (hasattr(plugin_type, '__module__') and
|
||||
issubclass(plugin_type, CMSPluginBase)):
|
||||
plugin_model = plugin_type.model
|
||||
assert plugin_type in plugin_pool.plugins.values()
|
||||
plugin_type = plugin_type.__name__
|
||||
elif isinstance(plugin_type, six.string_types):
|
||||
try:
|
||||
plugin_model = plugin_pool.get_plugin(plugin_type).model
|
||||
except KeyError:
|
||||
raise TypeError(
|
||||
'plugin_type must be CMSPluginBase subclass or string'
|
||||
)
|
||||
else:
|
||||
raise TypeError('plugin_type must be CMSPluginBase subclass or string')
|
||||
return plugin_model, plugin_type
|
||||
|
||||
|
||||
#===============================================================================
|
||||
# Public API
|
||||
#===============================================================================
|
||||
|
||||
@transaction.atomic
|
||||
def create_page(title, template, language, menu_title=None, slug=None,
|
||||
apphook=None, apphook_namespace=None, redirect=None, meta_description=None,
|
||||
created_by='python-api', parent=None,
|
||||
publication_date=None, publication_end_date=None,
|
||||
in_navigation=False, soft_root=False, reverse_id=None,
|
||||
navigation_extenders=None, published=False, site=None,
|
||||
login_required=False, limit_visibility_in_menu=constants.VISIBILITY_ALL,
|
||||
position="last-child", overwrite_url=None,
|
||||
xframe_options=Page.X_FRAME_OPTIONS_INHERIT, page_title=None):
|
||||
"""
|
||||
Create a CMS Page and it's title for the given language
|
||||
|
||||
See docs/extending_cms/api_reference.rst for more info
|
||||
"""
|
||||
# validate template
|
||||
if not template == TEMPLATE_INHERITANCE_MAGIC:
|
||||
assert template in [tpl[0] for tpl in get_cms_setting('TEMPLATES')]
|
||||
get_template(template)
|
||||
|
||||
# validate site
|
||||
if not site:
|
||||
site = get_current_site()
|
||||
else:
|
||||
assert isinstance(site, Site)
|
||||
|
||||
# validate language:
|
||||
assert language in get_language_list(site), get_cms_setting('LANGUAGES').get(site.pk)
|
||||
|
||||
# validate parent
|
||||
if parent:
|
||||
assert isinstance(parent, Page)
|
||||
assert parent.publisher_is_draft
|
||||
|
||||
# validate publication date
|
||||
if publication_date:
|
||||
assert isinstance(publication_date, datetime.date)
|
||||
|
||||
# validate publication end date
|
||||
if publication_end_date:
|
||||
assert isinstance(publication_end_date, datetime.date)
|
||||
|
||||
if navigation_extenders:
|
||||
raw_menus = menu_pool.get_menus_by_attribute("cms_enabled", True)
|
||||
menus = [menu[0] for menu in raw_menus]
|
||||
assert navigation_extenders in menus
|
||||
|
||||
# validate menu visibility
|
||||
accepted_limitations = (constants.VISIBILITY_ALL, constants.VISIBILITY_USERS, constants.VISIBILITY_ANONYMOUS)
|
||||
assert limit_visibility_in_menu in accepted_limitations
|
||||
|
||||
# validate position
|
||||
assert position in ('last-child', 'first-child', 'left', 'right')
|
||||
target_node = parent.node if parent else None
|
||||
|
||||
# validate and normalize apphook
|
||||
if apphook:
|
||||
application_urls = _verify_apphook(apphook, apphook_namespace)
|
||||
else:
|
||||
application_urls = None
|
||||
|
||||
# ugly permissions hack
|
||||
if created_by and isinstance(created_by, get_user_model()):
|
||||
_thread_locals.user = created_by
|
||||
created_by = getattr(created_by, get_user_model().USERNAME_FIELD)
|
||||
else:
|
||||
_thread_locals.user = None
|
||||
|
||||
if reverse_id:
|
||||
if Page.objects.drafts().filter(reverse_id=reverse_id, node__site=site).exists():
|
||||
raise FieldError('A page with the reverse_id="%s" already exist.' % reverse_id)
|
||||
|
||||
page = Page(
|
||||
created_by=created_by,
|
||||
changed_by=created_by,
|
||||
publication_date=publication_date,
|
||||
publication_end_date=publication_end_date,
|
||||
in_navigation=in_navigation,
|
||||
soft_root=soft_root,
|
||||
reverse_id=reverse_id,
|
||||
navigation_extenders=navigation_extenders,
|
||||
template=template,
|
||||
application_urls=application_urls,
|
||||
application_namespace=apphook_namespace,
|
||||
login_required=login_required,
|
||||
limit_visibility_in_menu=limit_visibility_in_menu,
|
||||
xframe_options=xframe_options,
|
||||
)
|
||||
page.set_tree_node(site=site, target=target_node, position=position)
|
||||
page.save()
|
||||
page.rescan_placeholders()
|
||||
|
||||
create_title(
|
||||
language=language,
|
||||
title=title,
|
||||
page_title=page_title,
|
||||
menu_title=menu_title,
|
||||
slug=slug,
|
||||
redirect=redirect,
|
||||
meta_description=meta_description,
|
||||
page=page,
|
||||
overwrite_url=overwrite_url,
|
||||
)
|
||||
|
||||
if published:
|
||||
page.publish(language)
|
||||
|
||||
if parent and position in ('last-child', 'first-child'):
|
||||
parent._clear_node_cache()
|
||||
|
||||
del _thread_locals.user
|
||||
return page
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_title(language, title, page, menu_title=None, slug=None,
|
||||
redirect=None, meta_description=None, parent=None,
|
||||
overwrite_url=None, page_title=None, path=None):
|
||||
"""
|
||||
Create a title.
|
||||
|
||||
Parent is only used if slug=None.
|
||||
|
||||
See docs/extending_cms/api_reference.rst for more info
|
||||
"""
|
||||
# validate page
|
||||
assert isinstance(page, Page)
|
||||
|
||||
# validate language:
|
||||
assert language in get_language_list(page.node.site_id)
|
||||
|
||||
# set default slug:
|
||||
if not slug:
|
||||
base = page.get_path_for_slug(slugify(title), language)
|
||||
slug = get_available_slug(page.node.site, base, language)
|
||||
|
||||
if overwrite_url:
|
||||
path = overwrite_url.strip('/')
|
||||
elif path is None:
|
||||
path = page.get_path_for_slug(slug, language)
|
||||
|
||||
title = Title.objects.create(
|
||||
language=language,
|
||||
title=title,
|
||||
menu_title=menu_title,
|
||||
page_title=page_title,
|
||||
slug=slug,
|
||||
path=path,
|
||||
redirect=redirect,
|
||||
meta_description=meta_description,
|
||||
page=page,
|
||||
has_url_overwrite=bool(overwrite_url),
|
||||
)
|
||||
|
||||
page_languages = page.get_languages()
|
||||
|
||||
if language not in page_languages:
|
||||
page.update_languages(page_languages + [language])
|
||||
return title
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def add_plugin(placeholder, plugin_type, language, position='last-child',
|
||||
target=None, **data):
|
||||
"""
|
||||
Add a plugin to a placeholder
|
||||
|
||||
See docs/extending_cms/api_reference.rst for more info
|
||||
"""
|
||||
# validate placeholder
|
||||
assert isinstance(placeholder, Placeholder)
|
||||
|
||||
# validate and normalize plugin type
|
||||
plugin_model, plugin_type = _verify_plugin_type(plugin_type)
|
||||
if target:
|
||||
if position == 'last-child':
|
||||
if CMSPlugin.node_order_by:
|
||||
position = 'sorted-child'
|
||||
new_pos = CMSPlugin.objects.filter(parent=target).count()
|
||||
parent_id = target.pk
|
||||
elif position == 'first-child':
|
||||
new_pos = 0
|
||||
if CMSPlugin.node_order_by:
|
||||
position = 'sorted-child'
|
||||
parent_id = target.pk
|
||||
elif position == 'left':
|
||||
new_pos = target.position
|
||||
if CMSPlugin.node_order_by:
|
||||
position = 'sorted-sibling'
|
||||
parent_id = target.parent_id
|
||||
elif position == 'right':
|
||||
new_pos = target.position + 1
|
||||
if CMSPlugin.node_order_by:
|
||||
position = 'sorted-sibling'
|
||||
parent_id = target.parent_id
|
||||
else:
|
||||
raise Exception('position not supported: %s' % position)
|
||||
if position == 'last-child' or position == 'first-child':
|
||||
qs = CMSPlugin.objects.filter(language=language, parent=target, position__gte=new_pos,
|
||||
placeholder=placeholder)
|
||||
else:
|
||||
qs = CMSPlugin.objects.filter(language=language, parent=target.parent_id, position__gte=new_pos,
|
||||
placeholder=placeholder)
|
||||
for pl in qs:
|
||||
pl.position += 1
|
||||
pl.save()
|
||||
else:
|
||||
if position == 'last-child':
|
||||
new_pos = CMSPlugin.objects.filter(language=language, parent__isnull=True, placeholder=placeholder).count()
|
||||
else:
|
||||
new_pos = 0
|
||||
for pl in CMSPlugin.objects.filter(language=language, parent__isnull=True, position__gte=new_pos,
|
||||
placeholder=placeholder):
|
||||
pl.position += 1
|
||||
pl.save()
|
||||
parent_id = None
|
||||
plugin_base = CMSPlugin(
|
||||
plugin_type=plugin_type,
|
||||
placeholder=placeholder,
|
||||
position=new_pos,
|
||||
language=language,
|
||||
parent_id=parent_id,
|
||||
)
|
||||
|
||||
plugin_base = plugin_base.add_root(instance=plugin_base)
|
||||
|
||||
if target:
|
||||
plugin_base = plugin_base.move(target, pos=position)
|
||||
plugin = plugin_model(**data)
|
||||
plugin_base.set_base_attr(plugin)
|
||||
plugin.save()
|
||||
return plugin
|
||||
|
||||
|
||||
def create_page_user(created_by, user,
|
||||
can_add_page=True, can_view_page=True,
|
||||
can_change_page=True, can_delete_page=True,
|
||||
can_recover_page=True, can_add_pageuser=True,
|
||||
can_change_pageuser=True, can_delete_pageuser=True,
|
||||
can_add_pagepermission=True,
|
||||
can_change_pagepermission=True,
|
||||
can_delete_pagepermission=True, grant_all=False):
|
||||
"""
|
||||
Creates a page user.
|
||||
|
||||
See docs/extending_cms/api_reference.rst for more info
|
||||
"""
|
||||
from cms.admin.forms import save_permissions
|
||||
if grant_all:
|
||||
# just be lazy
|
||||
return create_page_user(created_by, user, True, True, True, True,
|
||||
True, True, True, True, True, True, True)
|
||||
|
||||
# validate created_by
|
||||
assert isinstance(created_by, get_user_model())
|
||||
|
||||
data = {
|
||||
'can_add_page': can_add_page,
|
||||
'can_view_page': can_view_page,
|
||||
'can_change_page': can_change_page,
|
||||
'can_delete_page': can_delete_page,
|
||||
'can_recover_page': can_recover_page,
|
||||
'can_add_pageuser': can_add_pageuser,
|
||||
'can_change_pageuser': can_change_pageuser,
|
||||
'can_delete_pageuser': can_delete_pageuser,
|
||||
'can_add_pagepermission': can_add_pagepermission,
|
||||
'can_change_pagepermission': can_change_pagepermission,
|
||||
'can_delete_pagepermission': can_delete_pagepermission,
|
||||
}
|
||||
user.is_staff = True
|
||||
user.is_active = True
|
||||
page_user = PageUser(created_by=created_by)
|
||||
for field in [f.name for f in get_user_model()._meta.local_fields]:
|
||||
setattr(page_user, field, getattr(user, field))
|
||||
user.save()
|
||||
page_user.save()
|
||||
save_permissions(data, page_user)
|
||||
return user
|
||||
|
||||
|
||||
def assign_user_to_page(page, user, grant_on=ACCESS_PAGE_AND_DESCENDANTS,
|
||||
can_add=False, can_change=False, can_delete=False,
|
||||
can_change_advanced_settings=False, can_publish=False,
|
||||
can_change_permissions=False, can_move_page=False,
|
||||
can_recover_page=True, can_view=False,
|
||||
grant_all=False, global_permission=False):
|
||||
"""
|
||||
Assigns given user to page, and gives him requested permissions.
|
||||
|
||||
See docs/extending_cms/api_reference.rst for more info
|
||||
"""
|
||||
grant_all = grant_all and not global_permission
|
||||
data = {
|
||||
'can_add': can_add or grant_all,
|
||||
'can_change': can_change or grant_all,
|
||||
'can_delete': can_delete or grant_all,
|
||||
'can_change_advanced_settings': can_change_advanced_settings or grant_all,
|
||||
'can_publish': can_publish or grant_all,
|
||||
'can_change_permissions': can_change_permissions or grant_all,
|
||||
'can_move_page': can_move_page or grant_all,
|
||||
'can_view': can_view or grant_all,
|
||||
}
|
||||
|
||||
page_permission = PagePermission(page=page, user=user,
|
||||
grant_on=grant_on, **data)
|
||||
page_permission.save()
|
||||
if global_permission:
|
||||
page_permission = GlobalPagePermission(
|
||||
user=user, can_recover_page=can_recover_page, **data)
|
||||
page_permission.save()
|
||||
page_permission.sites.add(get_current_site())
|
||||
return page_permission
|
||||
|
||||
|
||||
def publish_page(page, user, language):
|
||||
"""
|
||||
Publish a page. This sets `page.published` to `True` and calls publish()
|
||||
which does the actual publishing.
|
||||
|
||||
See docs/extending_cms/api_reference.rst for more info
|
||||
"""
|
||||
page = page.reload()
|
||||
|
||||
if not page.has_publish_permission(user):
|
||||
raise PermissionDenied()
|
||||
# Set the current_user to have the page's changed_by
|
||||
# attribute set correctly.
|
||||
# 'user' is a user object, but current_user() just wants the username (a string).
|
||||
with current_user(user.get_username()):
|
||||
page.publish(language)
|
||||
return page.reload()
|
||||
|
||||
|
||||
def publish_pages(include_unpublished=False, language=None, site=None):
|
||||
"""
|
||||
Create published public version of selected drafts.
|
||||
"""
|
||||
qs = Page.objects.drafts()
|
||||
|
||||
if not include_unpublished:
|
||||
qs = qs.filter(title_set__published=True).distinct()
|
||||
|
||||
if site:
|
||||
qs = qs.filter(node__site=site)
|
||||
|
||||
output_language = None
|
||||
for i, page in enumerate(qs):
|
||||
add = True
|
||||
titles = page.title_set
|
||||
if not include_unpublished:
|
||||
titles = titles.filter(published=True)
|
||||
for lang in titles.values_list("language", flat=True):
|
||||
if language is None or lang == language:
|
||||
if not output_language:
|
||||
output_language = lang
|
||||
if not page.publish(lang):
|
||||
add = False
|
||||
# we may need to activate the first (main) language for proper page title rendering
|
||||
activate(output_language)
|
||||
yield (page, add)
|
||||
|
||||
|
||||
def get_page_draft(page):
|
||||
"""
|
||||
Returns the draft version of a page, regardless if the passed in
|
||||
page is a published version or a draft version.
|
||||
|
||||
:param page: The page to get the draft version
|
||||
:type page: :class:`cms.models.pagemodel.Page` instance
|
||||
:return page: draft version of the page
|
||||
:type page: :class:`cms.models.pagemodel.Page` instance
|
||||
"""
|
||||
if page:
|
||||
if page.publisher_is_draft:
|
||||
return page
|
||||
else:
|
||||
return page.publisher_public
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def copy_plugins_to_language(page, source_language, target_language,
|
||||
only_empty=True):
|
||||
"""
|
||||
Copy the plugins to another language in the same page for all the page
|
||||
placeholders.
|
||||
|
||||
By default plugins are copied only if placeholder has no plugin for the
|
||||
target language; use ``only_empty=False`` to change this.
|
||||
|
||||
.. warning: This function skips permissions checks
|
||||
|
||||
:param page: the page to copy
|
||||
:type page: :class:`cms.models.pagemodel.Page` instance
|
||||
:param string source_language: The source language code,
|
||||
must be in :setting:`django:LANGUAGES`
|
||||
:param string target_language: The source language code,
|
||||
must be in :setting:`django:LANGUAGES`
|
||||
:param bool only_empty: if False, plugin are copied even if
|
||||
plugins exists in the target language (on a placeholder basis).
|
||||
:return int: number of copied plugins
|
||||
"""
|
||||
copied = 0
|
||||
placeholders = page.get_placeholders()
|
||||
for placeholder in placeholders:
|
||||
# only_empty is True we check if the placeholder already has plugins and
|
||||
# we skip it if has some
|
||||
if not only_empty or not placeholder.get_plugins(language=target_language).exists():
|
||||
plugins = list(
|
||||
placeholder.get_plugins(language=source_language).order_by('path'))
|
||||
copied_plugins = copy_plugins.copy_plugins_to(plugins, placeholder, target_language)
|
||||
copied += len(copied_plugins)
|
||||
return copied
|
||||
|
||||
|
||||
def can_change_page(request):
|
||||
"""
|
||||
Check whether a user has the permission to change the page.
|
||||
|
||||
This will work across all permission-related setting, with a unified interface
|
||||
to permission checking.
|
||||
"""
|
||||
from cms.utils import page_permissions
|
||||
|
||||
user = request.user
|
||||
current_page = request.current_page
|
||||
|
||||
if current_page:
|
||||
return page_permissions.user_can_change_page(user, current_page)
|
||||
|
||||
site = Site.objects.get_current(request)
|
||||
return page_permissions.user_can_change_all_pages(user, site)
|
||||
@@ -1,95 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class CMSApp(object):
|
||||
#: list of urlconfs: example: ``_urls = ["myapp.urls"]``
|
||||
_urls = []
|
||||
#: list of menu classes: example: ``_menus = [MyAppMenu]``
|
||||
_menus = []
|
||||
#: name of the apphook (required)
|
||||
name = None
|
||||
#: name of the app, this enables Django namespaces support (optional)
|
||||
app_name = None
|
||||
#: configuration model (optional)
|
||||
app_config = None
|
||||
#: if set to true, apphook inherits permissions from the current page
|
||||
permissions = True
|
||||
#: list of application names to exclude from inheriting CMS permissions
|
||||
exclude_permissions = []
|
||||
|
||||
def __new__(cls):
|
||||
"""
|
||||
We want to bind the CMSapp class to a specific AppHookConfig, but only one at a time
|
||||
Checking for the runtime attribute should be a sane fix
|
||||
"""
|
||||
if cls.app_config:
|
||||
if getattr(cls.app_config, 'cmsapp', None) and cls.app_config.cmsapp != cls:
|
||||
raise RuntimeError(
|
||||
'Only one AppHook per AppHookConfiguration must exists.\n'
|
||||
'AppHook %s already defined for %s AppHookConfig' % (
|
||||
cls.app_config.cmsapp.__name__, cls.app_config.__name__
|
||||
)
|
||||
)
|
||||
cls.app_config.cmsapp = cls
|
||||
return super(CMSApp, cls).__new__(cls)
|
||||
|
||||
def get_configs(self):
|
||||
"""
|
||||
Returns all the apphook configuration instances.
|
||||
"""
|
||||
raise NotImplemented('Configurable AppHooks must implement this method')
|
||||
|
||||
def get_config(self, namespace):
|
||||
"""
|
||||
Returns the apphook configuration instance linked to the given namespace
|
||||
"""
|
||||
raise NotImplemented('Configurable AppHooks must implement this method')
|
||||
|
||||
def get_config_add_url(self):
|
||||
"""
|
||||
Returns the url to add a new apphook configuration instance
|
||||
(usually the model admin add view)
|
||||
"""
|
||||
raise NotImplemented('Configurable AppHooks must implement this method')
|
||||
|
||||
def get_menus(self, page=None, language=None, **kwargs):
|
||||
"""
|
||||
Returns the menus for the apphook instance, eventually selected according
|
||||
to the given arguments.
|
||||
|
||||
By default it returns the menus assigned to :py:attr:`CMSApp._menus`.
|
||||
|
||||
If no menus are returned, then the user will need to attach menus to pages
|
||||
manually in the admin.
|
||||
|
||||
This method must return all the menus used by this apphook if no arguments are
|
||||
provided. Example::
|
||||
|
||||
if page and page.reverse_id == 'page1':
|
||||
return [Menu1]
|
||||
elif page and page.reverse_id == 'page2':
|
||||
return [Menu2]
|
||||
else:
|
||||
return [Menu1, Menu2]
|
||||
|
||||
:param page: page the apphook is attached to
|
||||
:param language: current site language
|
||||
:return: list of menu classes
|
||||
"""
|
||||
return self._menus
|
||||
|
||||
def get_urls(self, page=None, language=None, **kwargs):
|
||||
"""
|
||||
Returns the urlconfs for the apphook instance, eventually selected
|
||||
according to the given arguments.
|
||||
|
||||
By default it returns the urls assigned to :py:attr:`CMSApp._urls`
|
||||
|
||||
This method **must** return a non empty list of urlconfs,
|
||||
even if no argument is passed.
|
||||
|
||||
:param page: page the apphook is attached to
|
||||
:param language: current site language
|
||||
:return: list of urlconfs strings
|
||||
"""
|
||||
return self._urls
|
||||
@@ -1,102 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.module_loading import autodiscover_modules, import_string
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from cms.app_base import CMSApp
|
||||
from cms.exceptions import AppAlreadyRegistered
|
||||
from cms.utils.conf import get_cms_setting
|
||||
|
||||
|
||||
class ApphookPool(object):
|
||||
|
||||
def __init__(self):
|
||||
self.apphooks = []
|
||||
self.apps = {}
|
||||
self.discovered = False
|
||||
|
||||
def clear(self):
|
||||
# TODO: remove this method, it's Python, we don't need it.
|
||||
self.apphooks = []
|
||||
self.apps = {}
|
||||
self.discovered = False
|
||||
|
||||
def register(self, app=None, discovering_apps=False):
|
||||
# allow use as a decorator
|
||||
if app is None:
|
||||
return lambda app: self.register(app, discovering_apps)
|
||||
|
||||
if self.apphooks and not discovering_apps:
|
||||
return app
|
||||
|
||||
if app.__name__ in self.apps:
|
||||
raise AppAlreadyRegistered(
|
||||
'A CMS application %r is already registered' % app.__name__)
|
||||
|
||||
if not issubclass(app, CMSApp):
|
||||
raise ImproperlyConfigured(
|
||||
'CMS application must inherit from cms.app_base.CMSApp, '
|
||||
'but %r does not' % app.__name__)
|
||||
|
||||
if not hasattr(app, 'menus') and hasattr(app, 'menu'):
|
||||
warnings.warn("You define a 'menu' attribute on CMS application "
|
||||
"%r, but the 'menus' attribute is empty, "
|
||||
"did you make a typo?" % app.__name__)
|
||||
|
||||
self.apps[app.__name__] = app()
|
||||
return app
|
||||
|
||||
def discover_apps(self):
|
||||
self.apphooks = get_cms_setting('APPHOOKS')
|
||||
|
||||
if self.apphooks:
|
||||
for path in self.apphooks:
|
||||
cls = import_string(path)
|
||||
try:
|
||||
self.register(cls, discovering_apps=True)
|
||||
except AppAlreadyRegistered:
|
||||
pass
|
||||
|
||||
else:
|
||||
autodiscover_modules('cms_apps')
|
||||
|
||||
self.discovered = True
|
||||
|
||||
def get_apphooks(self):
|
||||
hooks = []
|
||||
|
||||
if not self.discovered:
|
||||
self.discover_apps()
|
||||
|
||||
for app_name in self.apps:
|
||||
app = self.apps[app_name]
|
||||
|
||||
if app.get_urls():
|
||||
hooks.append((app_name, app.name))
|
||||
|
||||
# Unfortunately, we lose the ordering since we now have a list of
|
||||
# tuples. Let's reorder by app_name:
|
||||
hooks = sorted(hooks, key=lambda hook: hook[1])
|
||||
|
||||
return hooks
|
||||
|
||||
def get_apphook(self, app_name):
|
||||
if not self.discovered:
|
||||
self.discover_apps()
|
||||
|
||||
try:
|
||||
return self.apps[app_name]
|
||||
except KeyError:
|
||||
# deprecated: return apphooks registered in db with urlconf name
|
||||
# instead of apphook class name
|
||||
for app in self.apps.values():
|
||||
if app_name in app.get_urls():
|
||||
return app
|
||||
|
||||
warnings.warn(_('No registered apphook "%r" found') % app_name)
|
||||
return None
|
||||
|
||||
|
||||
apphook_pool = ApphookPool()
|
||||
@@ -1,273 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import OrderedDict
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import OperationalError, ProgrammingError
|
||||
from django.utils import six
|
||||
from django.utils.translation import get_language, override
|
||||
from django.urls import Resolver404, reverse
|
||||
|
||||
from cms.apphook_pool import apphook_pool
|
||||
from cms.models.pagemodel import Page
|
||||
from cms.utils import get_current_site
|
||||
from cms.utils.compat import DJANGO_1_11
|
||||
from cms.utils.compat.dj import RegexPattern, URLPattern, URLResolver
|
||||
from cms.utils.i18n import get_language_list
|
||||
from cms.utils.moderator import use_draft
|
||||
|
||||
|
||||
APP_RESOLVERS = []
|
||||
|
||||
|
||||
def clear_app_resolvers():
|
||||
global APP_RESOLVERS
|
||||
APP_RESOLVERS = []
|
||||
|
||||
|
||||
def applications_page_check(request, current_page=None, path=None):
|
||||
"""Tries to find if given path was resolved over application.
|
||||
Applications have higher priority than other cms pages.
|
||||
"""
|
||||
if current_page:
|
||||
return current_page
|
||||
if path is None:
|
||||
# We should get in this branch only if an apphook is active on /
|
||||
# This removes the non-CMS part of the URL.
|
||||
path = request.path_info.replace(reverse('pages-root'), '', 1)
|
||||
# check if application resolver can resolve this
|
||||
for lang in get_language_list():
|
||||
if path.startswith(lang + "/"):
|
||||
path = path[len(lang + "/"):]
|
||||
|
||||
use_public = not use_draft(request)
|
||||
|
||||
for resolver in APP_RESOLVERS:
|
||||
try:
|
||||
page_id = resolver.resolve_page_id(path)
|
||||
# yes, it is application page
|
||||
page = Page.objects.public().get(id=page_id)
|
||||
# If current page was matched, then we have some override for
|
||||
# content from cms, but keep current page. Otherwise return page
|
||||
# to which was application assigned.
|
||||
return page if use_public else page.publisher_public
|
||||
except Resolver404:
|
||||
# Raised if the page is not managed by an apphook
|
||||
pass
|
||||
except Page.DoesNotExist:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class AppRegexURLResolver(URLResolver):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.page_id = None
|
||||
self.url_patterns_dict = {}
|
||||
super(AppRegexURLResolver, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def url_patterns(self):
|
||||
language = get_language()
|
||||
if language in self.url_patterns_dict:
|
||||
return self.url_patterns_dict[language]
|
||||
else:
|
||||
return []
|
||||
|
||||
def resolve_page_id(self, path):
|
||||
"""Resolves requested path similar way how resolve does, but instead
|
||||
of return callback,.. returns page_id to which was application
|
||||
assigned.
|
||||
"""
|
||||
tried = []
|
||||
pattern = getattr(self, 'pattern', self)
|
||||
match = pattern.regex.search(path)
|
||||
if match:
|
||||
new_path = path[match.end():]
|
||||
for pattern in self.url_patterns:
|
||||
if isinstance(pattern, AppRegexURLResolver):
|
||||
try:
|
||||
return pattern.resolve_page_id(new_path)
|
||||
except Resolver404:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
sub_match = pattern.resolve(new_path)
|
||||
except Resolver404 as e:
|
||||
tried_match = e.args[0].get('tried')
|
||||
if tried_match is not None:
|
||||
tried.extend([[pattern] + t for t in tried_match])
|
||||
else:
|
||||
tried.extend([pattern])
|
||||
else:
|
||||
if sub_match:
|
||||
return getattr(pattern, 'page_id', None)
|
||||
pattern = getattr(pattern, 'pattern', pattern)
|
||||
tried.append(pattern.regex.pattern)
|
||||
raise Resolver404({'tried': tried, 'path': new_path})
|
||||
|
||||
|
||||
def recurse_patterns(path, pattern_list, page_id, default_args=None,
|
||||
nested=False):
|
||||
"""
|
||||
Recurse over a list of to-be-hooked patterns for a given path prefix
|
||||
"""
|
||||
newpatterns = []
|
||||
for pattern in pattern_list:
|
||||
app_pat = getattr(pattern, 'pattern', pattern).regex.pattern
|
||||
# make sure we don't get patterns that start with more than one '^'!
|
||||
app_pat = app_pat.lstrip('^')
|
||||
path = path.lstrip('^')
|
||||
regex = r'^%s%s' % (path, app_pat) if not nested else r'^%s' % (app_pat)
|
||||
if isinstance(pattern, URLResolver):
|
||||
# include default_args
|
||||
args = pattern.default_kwargs
|
||||
if default_args:
|
||||
args.update(default_args)
|
||||
# see lines 243 and 236 of urlresolvers.py to understand the next line
|
||||
urlconf_module = recurse_patterns(regex, pattern.url_patterns, page_id, args, nested=True)
|
||||
# this is an 'include', recurse!
|
||||
regex_pattern = regex
|
||||
if not DJANGO_1_11:
|
||||
regex_pattern = RegexPattern(regex)
|
||||
resolver = URLResolver(regex_pattern, urlconf_module,
|
||||
pattern.default_kwargs, pattern.app_name,
|
||||
pattern.namespace)
|
||||
else:
|
||||
# Re-do the URLPattern with the new regular expression
|
||||
args = pattern.default_args
|
||||
if default_args:
|
||||
args.update(default_args)
|
||||
|
||||
regex_pattern = regex
|
||||
if not DJANGO_1_11:
|
||||
regex_pattern = RegexPattern(regex, name=pattern.name)
|
||||
resolver = URLPattern(regex_pattern, pattern.callback, args,
|
||||
pattern.name)
|
||||
resolver.page_id = page_id
|
||||
newpatterns.append(resolver)
|
||||
return newpatterns
|
||||
|
||||
|
||||
def _set_permissions(patterns, exclude_permissions):
|
||||
for pattern in patterns:
|
||||
if isinstance(pattern, URLResolver):
|
||||
if pattern.namespace in exclude_permissions:
|
||||
continue
|
||||
_set_permissions(pattern.url_patterns, exclude_permissions)
|
||||
else:
|
||||
from cms.utils.decorators import cms_perms
|
||||
pattern.callback = cms_perms(pattern.callback)
|
||||
|
||||
|
||||
def get_app_urls(urls):
|
||||
for urlconf in urls:
|
||||
if isinstance(urlconf, six.string_types):
|
||||
mod = import_module(urlconf)
|
||||
if not hasattr(mod, 'urlpatterns'):
|
||||
raise ImproperlyConfigured(
|
||||
"URLConf `%s` has no urlpatterns attribute" % urlconf)
|
||||
yield getattr(mod, 'urlpatterns')
|
||||
elif isinstance(urlconf, (list, tuple)):
|
||||
yield urlconf
|
||||
else:
|
||||
yield [urlconf]
|
||||
|
||||
|
||||
def get_patterns_for_title(path, title):
|
||||
"""
|
||||
Resolve the urlconf module for a path+title combination
|
||||
Returns a list of url objects.
|
||||
"""
|
||||
app = apphook_pool.get_apphook(title.page.application_urls)
|
||||
url_patterns = []
|
||||
for pattern_list in get_app_urls(app.get_urls(title.page, title.language)):
|
||||
if path and not path.endswith('/'):
|
||||
path += '/'
|
||||
page_id = title.page.id
|
||||
url_patterns += recurse_patterns(path, pattern_list, page_id)
|
||||
return url_patterns
|
||||
|
||||
|
||||
def get_app_patterns():
|
||||
try:
|
||||
site = get_current_site()
|
||||
return _get_app_patterns(site)
|
||||
except (OperationalError, ProgrammingError):
|
||||
# ignore if DB is not ready
|
||||
# Starting with Django 1.9 this code gets called even when creating
|
||||
# or running migrations. So in many cases the DB will not be ready yet.
|
||||
return []
|
||||
|
||||
|
||||
def _get_app_patterns(site):
|
||||
"""
|
||||
Get a list of patterns for all hooked apps.
|
||||
|
||||
How this works:
|
||||
|
||||
By looking through all titles with an app hook (application_urls) we find
|
||||
all urlconf modules we have to hook into titles.
|
||||
|
||||
If we use the ML URL Middleware, we namespace those patterns with the title
|
||||
language.
|
||||
|
||||
All 'normal' patterns from the urlconf get re-written by prefixing them with
|
||||
the title path and then included into the cms url patterns.
|
||||
|
||||
If the app is still configured, but is no longer installed/available, then
|
||||
this method returns a degenerate patterns object: patterns('')
|
||||
"""
|
||||
from cms.models import Title
|
||||
|
||||
included = []
|
||||
|
||||
# we don't have a request here so get_page_queryset() can't be used,
|
||||
# so use public() queryset.
|
||||
# This can be done because url patterns are used just in frontend
|
||||
title_qs = Title.objects.public().filter(page__node__site=site)
|
||||
|
||||
hooked_applications = OrderedDict()
|
||||
|
||||
# Loop over all titles with an application hooked to them
|
||||
titles = (title_qs.exclude(page__application_urls=None)
|
||||
.exclude(page__application_urls='')
|
||||
.order_by('-page__node__path').select_related())
|
||||
# TODO: Need to be fixed for django-treebeard when forward ported to 3.1
|
||||
for title in titles:
|
||||
path = title.path
|
||||
mix_id = "%s:%s:%s" % (
|
||||
path + "/", title.page.application_urls, title.language)
|
||||
if mix_id in included:
|
||||
# don't add the same thing twice
|
||||
continue
|
||||
if not settings.APPEND_SLASH:
|
||||
path += '/'
|
||||
app = apphook_pool.get_apphook(title.page.application_urls)
|
||||
if not app:
|
||||
continue
|
||||
if title.page_id not in hooked_applications:
|
||||
hooked_applications[title.page_id] = {}
|
||||
app_ns = app.app_name, title.page.application_namespace
|
||||
with override(title.language):
|
||||
hooked_applications[title.page_id][title.language] = (
|
||||
app_ns, get_patterns_for_title(path, title), app)
|
||||
included.append(mix_id)
|
||||
# Build the app patterns to be included in the cms urlconfs
|
||||
app_patterns = []
|
||||
for page_id in hooked_applications.keys():
|
||||
resolver = None
|
||||
for lang in hooked_applications[page_id].keys():
|
||||
(app_ns, inst_ns), current_patterns, app = hooked_applications[page_id][lang] # nopyflakes
|
||||
if not resolver:
|
||||
regex_pattern = RegexPattern(r'') if not DJANGO_1_11 else r''
|
||||
resolver = AppRegexURLResolver(
|
||||
regex_pattern, 'app_resolver', app_name=app_ns, namespace=inst_ns)
|
||||
resolver.page_id = page_id
|
||||
if app.permissions:
|
||||
_set_permissions(current_patterns, app.exclude_permissions)
|
||||
|
||||
resolver.url_patterns_dict[lang] = current_patterns
|
||||
app_patterns.append(resolver)
|
||||
APP_RESOLVERS.append(resolver)
|
||||
return app_patterns
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class CMSConfig(AppConfig):
|
||||
name = 'cms'
|
||||
verbose_name = _("django CMS")
|
||||
|
||||
def ready(self):
|
||||
from cms.utils.setup import setup
|
||||
|
||||
setup()
|
||||
Vendored
-87
@@ -1,87 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from cms.utils.conf import get_cms_setting
|
||||
|
||||
CMS_PAGE_CACHE_VERSION_KEY = get_cms_setting("CACHE_PREFIX") + '_PAGE_CACHE_VERSION'
|
||||
|
||||
|
||||
def _get_cache_version():
|
||||
"""
|
||||
Returns the current page cache version, explicitly setting one if not
|
||||
defined.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
|
||||
version = cache.get(CMS_PAGE_CACHE_VERSION_KEY)
|
||||
|
||||
if version:
|
||||
return version
|
||||
else:
|
||||
_set_cache_version(1)
|
||||
return 1
|
||||
|
||||
|
||||
def _set_cache_version(version):
|
||||
"""
|
||||
Set the cache version to the specified value.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
|
||||
cache.set(
|
||||
CMS_PAGE_CACHE_VERSION_KEY,
|
||||
version,
|
||||
get_cms_setting('CACHE_DURATIONS')['content']
|
||||
)
|
||||
|
||||
|
||||
def invalidate_cms_page_cache():
|
||||
"""
|
||||
Invalidates the CMS PAGE CACHE.
|
||||
"""
|
||||
|
||||
#
|
||||
# NOTE: We're using a cache versioning strategy for invalidating the page
|
||||
# cache when necessary. Instead of wiping all the old entries, we simply
|
||||
# increment the version number rendering all previous entries
|
||||
# inaccessible and left to expire naturally.
|
||||
#
|
||||
# ALSO NOTE: According to the Django documentation, a timeout value of
|
||||
# `None' (in version 1.6+) is supposed to mean "cache forever", however,
|
||||
# this is actually only implemented as only slightly less than 30 days in
|
||||
# some backends (memcached, in particular). In older Djangos, `None' means
|
||||
# "use default value". To avoid issues arising from different Django
|
||||
# versions and cache backend implementations, we will explicitly set the
|
||||
# lifespan of the CMS_PAGE_CACHE_VERSION entry to whatever is set in
|
||||
# settings.CACHE_DURATIONS['content']. This allows users to adjust as
|
||||
# necessary for their backend.
|
||||
#
|
||||
# To prevent writing cache entries that will live longer than our version
|
||||
# key, we will always re-write the current version number into the cache
|
||||
# just after we write any new cache entries, thus ensuring that the
|
||||
# version number will always outlive any entries written against that
|
||||
# version. This is a cheap operation.
|
||||
#
|
||||
# If there are no new cache writes before the version key expires, its
|
||||
# perfectly OK, since any previous entries cached against that version
|
||||
# will have also expired, so, it'd be pointless to try to access them
|
||||
# anyway.
|
||||
#
|
||||
version = _get_cache_version()
|
||||
_set_cache_version(version + 1)
|
||||
|
||||
|
||||
CLEAN_KEY_PATTERN = re.compile(r'[^a-zA-Z0-9_-]')
|
||||
|
||||
|
||||
def _clean_key(key):
|
||||
return CLEAN_KEY_PATTERN.sub('-', key)
|
||||
|
||||
|
||||
def _get_cache_key(name, page_lookup, lang, site_id):
|
||||
from cms.models import Page
|
||||
if isinstance(page_lookup, Page):
|
||||
page_key = str(page_lookup.pk)
|
||||
else:
|
||||
page_key = str(page_lookup)
|
||||
page_key = _clean_key(page_key)
|
||||
return get_cms_setting('CACHE_PREFIX') + name + '__page_lookup:' + page_key + '_site:' + str(site_id) + '_lang:' + str(lang)
|
||||
Vendored
-31
@@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
|
||||
from cms.utils.conf import get_cms_setting
|
||||
|
||||
|
||||
def _site_cache_key(lang):
|
||||
return "%s-%s" %(get_cms_setting('SITE_CHOICES_CACHE_KEY'), lang)
|
||||
|
||||
|
||||
def _page_cache_key(lang):
|
||||
return "%s-%s" %(get_cms_setting('PAGE_CHOICES_CACHE_KEY'), lang)
|
||||
|
||||
|
||||
def _clean_many(prefix):
|
||||
from django.core.cache import cache
|
||||
keys = []
|
||||
if settings.USE_I18N:
|
||||
for lang in [language[0] for language in settings.LANGUAGES]:
|
||||
keys.append("%s-%s" %(prefix, lang))
|
||||
else:
|
||||
keys = ["%s-%s" %(prefix, settings.LANGUAGE_CODE)]
|
||||
cache.delete_many(keys)
|
||||
|
||||
|
||||
def clean_site_choices_cache(sender, **kwargs):
|
||||
_clean_many(get_cms_setting('SITE_CHOICES_CACHE_KEY'))
|
||||
|
||||
|
||||
def clean_page_choices_cache(sender, **kwargs):
|
||||
_clean_many(get_cms_setting('PAGE_CHOICES_CACHE_KEY'))
|
||||
Vendored
-129
@@ -1,129 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import hashlib
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.cache import add_never_cache_headers, patch_response_headers, patch_vary_headers
|
||||
from django.utils.encoding import iri_to_uri
|
||||
from django.utils.timezone import now
|
||||
|
||||
from cms.cache import _get_cache_version, _set_cache_version, _get_cache_key
|
||||
from cms.constants import EXPIRE_NOW, MAX_EXPIRATION_TTL
|
||||
from cms.toolbar.utils import get_toolbar_from_request
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.helpers import get_timezone_name
|
||||
|
||||
|
||||
def _page_cache_key(request):
|
||||
#sha1 key of current path
|
||||
cache_key = "%s:%d:%s" % (
|
||||
get_cms_setting("CACHE_PREFIX"),
|
||||
settings.SITE_ID,
|
||||
hashlib.sha1(iri_to_uri(request.get_full_path()).encode('utf-8')).hexdigest()
|
||||
)
|
||||
if settings.USE_TZ:
|
||||
cache_key += '.%s' % get_timezone_name()
|
||||
return cache_key
|
||||
|
||||
|
||||
def set_page_cache(response):
|
||||
from django.core.cache import cache
|
||||
|
||||
request = response._request
|
||||
toolbar = get_toolbar_from_request(request)
|
||||
is_authenticated = request.user.is_authenticated
|
||||
|
||||
if is_authenticated or toolbar._cache_disabled or not get_cms_setting("PAGE_CACHE"):
|
||||
add_never_cache_headers(response)
|
||||
return response
|
||||
|
||||
# This *must* be TZ-aware
|
||||
timestamp = now()
|
||||
|
||||
placeholders = toolbar.content_renderer.get_rendered_placeholders()
|
||||
# Checks if there's a plugin using the legacy "cache = False"
|
||||
placeholder_ttl_list = []
|
||||
vary_cache_on_set = set()
|
||||
for ph in placeholders:
|
||||
# get_cache_expiration() always returns:
|
||||
# EXPIRE_NOW <= int <= MAX_EXPIRATION_IN_SECONDS
|
||||
ttl = ph.get_cache_expiration(request, timestamp)
|
||||
vary_cache_on = ph.get_vary_cache_on(request)
|
||||
|
||||
placeholder_ttl_list.append(ttl)
|
||||
if ttl and vary_cache_on:
|
||||
# We're only interested in vary headers if they come from
|
||||
# a cache-able placeholder.
|
||||
vary_cache_on_set |= set(vary_cache_on)
|
||||
|
||||
if EXPIRE_NOW not in placeholder_ttl_list:
|
||||
if placeholder_ttl_list:
|
||||
min_placeholder_ttl = min(x for x in placeholder_ttl_list)
|
||||
else:
|
||||
# Should only happen when there are no placeholders at all
|
||||
min_placeholder_ttl = MAX_EXPIRATION_TTL
|
||||
ttl = min(
|
||||
get_cms_setting('CACHE_DURATIONS')['content'],
|
||||
min_placeholder_ttl
|
||||
)
|
||||
|
||||
if ttl > 0:
|
||||
# Adds expiration, etc. to headers
|
||||
patch_response_headers(response, cache_timeout=ttl)
|
||||
patch_vary_headers(response, sorted(vary_cache_on_set))
|
||||
|
||||
version = _get_cache_version()
|
||||
# We also store the absolute expiration timestamp to avoid
|
||||
# recomputing it on cache-reads.
|
||||
expires_datetime = timestamp + timedelta(seconds=ttl)
|
||||
cache.set(
|
||||
_page_cache_key(request),
|
||||
(
|
||||
response.content,
|
||||
response._headers,
|
||||
expires_datetime,
|
||||
),
|
||||
ttl,
|
||||
version=version
|
||||
)
|
||||
# See note in invalidate_cms_page_cache()
|
||||
_set_cache_version(version)
|
||||
return response
|
||||
|
||||
|
||||
def get_page_cache(request):
|
||||
from django.core.cache import cache
|
||||
return cache.get(_page_cache_key(request), version=_get_cache_version())
|
||||
|
||||
|
||||
def get_xframe_cache(page):
|
||||
from django.core.cache import cache
|
||||
return cache.get('cms:xframe_options:%s' % page.pk)
|
||||
|
||||
|
||||
def set_xframe_cache(page, xframe_options):
|
||||
from django.core.cache import cache
|
||||
cache.set('cms:xframe_options:%s' % page.pk,
|
||||
xframe_options,
|
||||
version=_get_cache_version())
|
||||
_set_cache_version(_get_cache_version())
|
||||
|
||||
|
||||
def _page_url_key(page_lookup, lang, site_id):
|
||||
return _get_cache_key('page_url', page_lookup, lang, site_id) + '_type:absolute_url'
|
||||
|
||||
|
||||
def set_page_url_cache(page_lookup, lang, site_id, url):
|
||||
from django.core.cache import cache
|
||||
cache.set(_page_url_key(page_lookup, lang, site_id),
|
||||
url,
|
||||
get_cms_setting('CACHE_DURATIONS')['content'], version=_get_cache_version())
|
||||
_set_cache_version(_get_cache_version())
|
||||
|
||||
|
||||
def get_page_url_cache(page_lookup, lang, site_id):
|
||||
from django.core.cache import cache
|
||||
return cache.get(_page_url_key(page_lookup, lang, site_id),
|
||||
version=_get_cache_version())
|
||||
Vendored
-70
@@ -1,70 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from cms.utils.conf import get_cms_setting
|
||||
|
||||
|
||||
PERMISSION_KEYS = [
|
||||
'add_page', 'change_page', 'change_page_advanced_settings',
|
||||
'change_page_permissions', 'delete_page', 'move_page',
|
||||
'publish_page', 'view_page',
|
||||
]
|
||||
|
||||
|
||||
def get_cache_key(user, key):
|
||||
username = getattr(user, get_user_model().USERNAME_FIELD)
|
||||
return "%s:permission:%s:%s" % (
|
||||
get_cms_setting('CACHE_PREFIX'), username, key)
|
||||
|
||||
|
||||
def get_cache_permission_version_key():
|
||||
return "%s:permission:version" % (get_cms_setting('CACHE_PREFIX'),)
|
||||
|
||||
|
||||
def get_cache_permission_version():
|
||||
from django.core.cache import cache
|
||||
try:
|
||||
version = int(cache.get(get_cache_permission_version_key()))
|
||||
except Exception:
|
||||
version = 1
|
||||
return int(version)
|
||||
|
||||
|
||||
def get_permission_cache(user, key):
|
||||
"""
|
||||
Helper for reading values from cache
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
return cache.get(get_cache_key(user, key), version=get_cache_permission_version())
|
||||
|
||||
|
||||
def set_permission_cache(user, key, value):
|
||||
"""
|
||||
Helper method for storing values in cache. Stores used keys so
|
||||
all of them can be cleaned when clean_permission_cache gets called.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
# store this key, so we can clean it when required
|
||||
cache_key = get_cache_key(user, key)
|
||||
cache.set(cache_key, value,
|
||||
get_cms_setting('CACHE_DURATIONS')['permissions'],
|
||||
version=get_cache_permission_version())
|
||||
|
||||
|
||||
def clear_user_permission_cache(user):
|
||||
"""
|
||||
Cleans permission cache for given user.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
for key in PERMISSION_KEYS:
|
||||
cache.delete(get_cache_key(user, key), version=get_cache_permission_version())
|
||||
|
||||
|
||||
def clear_permission_cache():
|
||||
from django.core.cache import cache
|
||||
version = get_cache_permission_version()
|
||||
if version > 1:
|
||||
cache.incr(get_cache_permission_version_key())
|
||||
else:
|
||||
cache.set(get_cache_permission_version_key(), 2,
|
||||
get_cms_setting('CACHE_DURATIONS')['permissions'])
|
||||
Vendored
-178
@@ -1,178 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This module manages placeholder caching. We use a cache-versioning strategy
|
||||
in which each (placeholder x lang x site_id) manages its own version. The
|
||||
actual cache includes additional keys appropriate for the placeholders
|
||||
get_vary_cache_on().
|
||||
|
||||
Invalidation of a placeholder's cache simply increments the version number for
|
||||
the (placeholder x lang) pair, which renders any cache entries for that
|
||||
placeholder under that version inaccessible. Those cache entries will simply
|
||||
expire and will be purged according to the policy of the cache backend in-use.
|
||||
|
||||
The cache entries themselves may include additional sub-keys, according to the
|
||||
list of VARY header-names as returned by placeholder.get_vary_cache_on() and
|
||||
the current HTTPRequest object.
|
||||
|
||||
The vary-on header-names are also stored with the version. This enables us to
|
||||
check for cache hits without re-computing placeholder.get_vary_cache_on().
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from django.utils.timezone import now
|
||||
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.helpers import get_header_name, get_timezone_name
|
||||
|
||||
|
||||
def _get_placeholder_cache_version_key(placeholder, lang, site_id):
|
||||
"""
|
||||
Returns the version key for the given «placeholder», «lang» and «site_id».
|
||||
|
||||
Invalidating this (via clear_placeholder_cache by replacing the stored
|
||||
value with a new value) will effectively make all "sub-caches" relating to
|
||||
this (placeholder x lang) inaccessible. Sub-caches include caches per TZ
|
||||
and per VARY header.
|
||||
"""
|
||||
prefix = get_cms_setting('CACHE_PREFIX')
|
||||
key = '{prefix}|placeholder_cache_version|id:{id}|lang:{lang}|site:{site}'.format(
|
||||
prefix=prefix,
|
||||
id=placeholder.pk,
|
||||
lang=str(lang),
|
||||
site=site_id,
|
||||
)
|
||||
if len(key) > 250:
|
||||
key = '{prefix}|{hash}'.format(
|
||||
prefix=prefix,
|
||||
hash=hashlib.sha1(key.encode('utf-8')).hexdigest(),
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
def _get_placeholder_cache_version(placeholder, lang, site_id):
|
||||
"""
|
||||
Gets the (placeholder x lang)'s current version and vary-on header-names
|
||||
list, if present, otherwise resets to («timestamp», []).
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
|
||||
key = _get_placeholder_cache_version_key(placeholder, lang, site_id)
|
||||
cached = cache.get(key)
|
||||
if cached:
|
||||
version, vary_on_list = cached
|
||||
else:
|
||||
version = int(time.time() * 1000000)
|
||||
vary_on_list = []
|
||||
_set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list)
|
||||
return version, vary_on_list
|
||||
|
||||
|
||||
def _set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list=None, duration=None):
|
||||
"""
|
||||
Sets the (placeholder x lang)'s version and vary-on header-names list.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
|
||||
key = _get_placeholder_cache_version_key(placeholder, lang, site_id)
|
||||
|
||||
if not version or version < 1:
|
||||
version = int(time.time() * 1000000)
|
||||
|
||||
if vary_on_list is None:
|
||||
vary_on_list = []
|
||||
|
||||
cache.set(key, (version, vary_on_list), duration)
|
||||
|
||||
|
||||
def _get_placeholder_cache_key(placeholder, lang, site_id, request, soft=False):
|
||||
"""
|
||||
Returns the fully-addressed cache key for the given placeholder and
|
||||
the request.
|
||||
|
||||
The kwarg «soft» should be set to True if getting the cache key to then
|
||||
read from the cache. If instead the key retrieval is to support a cache
|
||||
write, let «soft» be False.
|
||||
"""
|
||||
prefix = get_cms_setting('CACHE_PREFIX')
|
||||
version, vary_on_list = _get_placeholder_cache_version(placeholder, lang, site_id)
|
||||
main_key = '{prefix}|render_placeholder|id:{id}|lang:{lang}|site:{site}|tz:{tz}|v:{version}'.format(
|
||||
prefix=prefix,
|
||||
id=placeholder.pk,
|
||||
lang=lang,
|
||||
site=site_id,
|
||||
tz=get_timezone_name(),
|
||||
version=version,
|
||||
)
|
||||
|
||||
if not soft:
|
||||
# We are about to write to the cache, so we want to get the latest
|
||||
# vary_cache_on headers and the correct cache expiration, ignoring any
|
||||
# we already have. If the placeholder has already been rendered, this
|
||||
# will be very efficient (zero-additional queries) due to the caching
|
||||
# of all its plugins during the rendering process anyway.
|
||||
vary_on_list = placeholder.get_vary_cache_on(request)
|
||||
duration = placeholder.get_cache_expiration(request, now())
|
||||
# Update the main placeholder cache version
|
||||
_set_placeholder_cache_version(
|
||||
placeholder, lang, site_id, version, vary_on_list, duration)
|
||||
|
||||
sub_key_list = []
|
||||
for key in vary_on_list:
|
||||
value = request.META.get(get_header_name(key)) or '_'
|
||||
sub_key_list.append(key + ':' + value)
|
||||
|
||||
cache_key = main_key
|
||||
if sub_key_list:
|
||||
cache_key += '|' + '|'.join(sub_key_list)
|
||||
|
||||
if len(cache_key) > 250:
|
||||
cache_key = '{prefix}|{hash}'.format(
|
||||
prefix=prefix,
|
||||
hash=hashlib.sha1(cache_key.encode('utf-8')).hexdigest(),
|
||||
)
|
||||
|
||||
return cache_key
|
||||
|
||||
|
||||
def set_placeholder_cache(placeholder, lang, site_id, content, request):
|
||||
"""
|
||||
Sets the (correct) placeholder cache with the rendered placeholder.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
|
||||
key = _get_placeholder_cache_key(placeholder, lang, site_id, request)
|
||||
|
||||
duration = min(
|
||||
get_cms_setting('CACHE_DURATIONS')['content'],
|
||||
placeholder.get_cache_expiration(request, now())
|
||||
)
|
||||
cache.set(key, content, duration)
|
||||
# "touch" the cache-version, so that it stays as fresh as this content.
|
||||
version, vary_on_list = _get_placeholder_cache_version(placeholder, lang, site_id)
|
||||
_set_placeholder_cache_version(
|
||||
placeholder, lang, site_id, version, vary_on_list, duration=duration)
|
||||
|
||||
|
||||
def get_placeholder_cache(placeholder, lang, site_id, request):
|
||||
"""
|
||||
Returns the placeholder from cache respecting the placeholder's
|
||||
VARY headers.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
|
||||
key = _get_placeholder_cache_key(placeholder, lang, site_id, request, soft=True)
|
||||
content = cache.get(key)
|
||||
return content
|
||||
|
||||
|
||||
def clear_placeholder_cache(placeholder, lang, site_id):
|
||||
"""
|
||||
Invalidates all existing cache entries for (placeholder x lang x site_id).
|
||||
We don't need to re-store the vary_on_list, because the cache is now
|
||||
effectively empty.
|
||||
"""
|
||||
version = int(time.time() * 1000000)
|
||||
_set_placeholder_cache_version(placeholder, lang, site_id, version, [])
|
||||
@@ -1,489 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.db.models.query import Prefetch, prefetch_related_objects
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.translation import override as force_language
|
||||
|
||||
from cms import constants
|
||||
from cms.api import get_page_draft
|
||||
from cms.apphook_pool import apphook_pool
|
||||
from cms.models import EmptyTitle
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.i18n import (
|
||||
get_fallback_languages,
|
||||
get_public_languages,
|
||||
hide_untranslated,
|
||||
is_valid_site_language,
|
||||
)
|
||||
from cms.utils.permissions import get_view_restrictions
|
||||
from cms.utils.page import get_page_queryset
|
||||
from cms.utils.page_permissions import user_can_view_all_pages
|
||||
|
||||
from menus.base import Menu, NavigationNode, Modifier
|
||||
from menus.menu_pool import menu_pool
|
||||
|
||||
|
||||
def get_visible_nodes(request, pages, site):
|
||||
"""
|
||||
This code is basically a many-pages-at-once version of
|
||||
cms.utils.page_permissions.user_can_view_page
|
||||
pages contains all published pages
|
||||
"""
|
||||
user = request.user
|
||||
_get_page_draft = get_page_draft
|
||||
public_for = get_cms_setting('PUBLIC_FOR')
|
||||
can_see_unrestricted = public_for == 'all' or (public_for == 'staff' and user.is_staff)
|
||||
|
||||
if not user.is_authenticated and not can_see_unrestricted:
|
||||
# User is not authenticated and can't see unrestricted pages,
|
||||
# no need to check for page restrictions because if there's some,
|
||||
# user is anon and if there is not any, user can't see unrestricted.
|
||||
return []
|
||||
|
||||
if user_can_view_all_pages(user, site):
|
||||
return list(pages)
|
||||
|
||||
# Permissions are only attached to draft pages
|
||||
draft_pages = [_get_page_draft(page) for page in pages]
|
||||
restricted_pages = get_view_restrictions(draft_pages)
|
||||
|
||||
if not restricted_pages:
|
||||
# If there's no restrictions, let the user see all pages
|
||||
# only if he can see unrestricted, otherwise return no pages.
|
||||
return list(pages) if can_see_unrestricted else []
|
||||
|
||||
user_id = user.pk
|
||||
user_groups = SimpleLazyObject(lambda: frozenset(user.groups.values_list('pk', flat=True)))
|
||||
is_auth_user = user.is_authenticated
|
||||
|
||||
def user_can_see_page(page):
|
||||
page_id = page.pk if page.publisher_is_draft else page.publisher_public_id
|
||||
page_permissions = restricted_pages.get(page_id, [])
|
||||
|
||||
if not page_permissions:
|
||||
# Page has no view restrictions, fallback to the project's
|
||||
# CMS_PUBLIC_FOR setting.
|
||||
return can_see_unrestricted
|
||||
|
||||
if not is_auth_user:
|
||||
return False
|
||||
|
||||
for perm in page_permissions:
|
||||
if perm.user_id == user_id or perm.group_id in user_groups:
|
||||
return True
|
||||
return False
|
||||
return [page for page in pages if user_can_see_page(page)]
|
||||
|
||||
|
||||
def get_menu_node_for_page(renderer, page, language, fallbacks=None):
|
||||
"""
|
||||
Transform a CMS page into a navigation node.
|
||||
|
||||
:param renderer: MenuRenderer instance bound to the request
|
||||
:param page: the page you wish to transform
|
||||
:param language: The current language used to render the menu
|
||||
"""
|
||||
if fallbacks is None:
|
||||
fallbacks = []
|
||||
|
||||
# Theses are simple to port over, since they are not calculated.
|
||||
# Other attributes will be added conditionally later.
|
||||
attr = {
|
||||
'is_page': True,
|
||||
'soft_root': page.soft_root,
|
||||
'auth_required': page.login_required,
|
||||
'reverse_id': page.reverse_id,
|
||||
}
|
||||
|
||||
if page.limit_visibility_in_menu is constants.VISIBILITY_ALL:
|
||||
attr['visible_for_authenticated'] = True
|
||||
attr['visible_for_anonymous'] = True
|
||||
else:
|
||||
attr['visible_for_authenticated'] = page.limit_visibility_in_menu == constants.VISIBILITY_USERS
|
||||
attr['visible_for_anonymous'] = page.limit_visibility_in_menu == constants.VISIBILITY_ANONYMOUS
|
||||
attr['is_home'] = page.is_home
|
||||
# Extenders can be either navigation extenders or from apphooks.
|
||||
extenders = []
|
||||
if page.navigation_extenders:
|
||||
if page.navigation_extenders in renderer.menus:
|
||||
extenders.append(page.navigation_extenders)
|
||||
elif "{0}:{1}".format(page.navigation_extenders, page.pk) in renderer.menus:
|
||||
extenders.append("{0}:{1}".format(page.navigation_extenders, page.pk))
|
||||
# Is this page an apphook? If so, we need to handle the apphooks's nodes
|
||||
# Only run this if we have a translation in the requested language for this
|
||||
# object. The title cache should have been prepopulated in CMSMenu.get_nodes
|
||||
# but otherwise, just request the title normally
|
||||
if page.title_cache.get(language) and page.application_urls:
|
||||
# it means it is an apphook
|
||||
app = apphook_pool.get_apphook(page.application_urls)
|
||||
|
||||
if app:
|
||||
extenders += app.get_menus(page, language)
|
||||
exts = []
|
||||
for ext in extenders:
|
||||
if hasattr(ext, "get_instances"):
|
||||
# CMSAttachMenus are treated a bit differently to allow them to be
|
||||
# able to be attached to multiple points in the navigation.
|
||||
exts.append("{0}:{1}".format(ext.__name__, page.pk))
|
||||
elif hasattr(ext, '__name__'):
|
||||
exts.append(ext.__name__)
|
||||
else:
|
||||
exts.append(ext)
|
||||
if exts:
|
||||
attr['navigation_extenders'] = exts
|
||||
|
||||
for lang in [language] + fallbacks:
|
||||
translation = page.title_cache[lang]
|
||||
|
||||
if translation:
|
||||
# Do we have a redirectURL?
|
||||
attr['redirect_url'] = translation.redirect # save redirect URL if any
|
||||
|
||||
# Now finally, build the NavigationNode object and return it.
|
||||
# The parent_id is manually set by the menu get_nodes method.
|
||||
ret_node = CMSNavigationNode(
|
||||
title=translation.menu_title or translation.title,
|
||||
url='',
|
||||
id=page.pk,
|
||||
attr=attr,
|
||||
visible=page.in_navigation,
|
||||
path=translation.path or translation.slug,
|
||||
language=(translation.language if translation.language != language else None),
|
||||
)
|
||||
return ret_node
|
||||
else:
|
||||
raise RuntimeError('Unable to render cms menu. There is a language misconfiguration.')
|
||||
|
||||
|
||||
class CMSNavigationNode(NavigationNode):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.path = kwargs.pop('path')
|
||||
# language is only used when we're dealing with a fallback
|
||||
self.language = kwargs.pop('language', None)
|
||||
super(CMSNavigationNode, self).__init__(*args, **kwargs)
|
||||
|
||||
def is_selected(self, request):
|
||||
try:
|
||||
page_id = request.current_page.pk
|
||||
except AttributeError:
|
||||
return False
|
||||
return page_id == self.id
|
||||
|
||||
def _get_absolute_url(self):
|
||||
if self.attr['is_home']:
|
||||
return reverse('pages-root')
|
||||
return reverse('pages-details-by-slug', kwargs={"slug": self.path})
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.language:
|
||||
with force_language(self.language):
|
||||
return self._get_absolute_url()
|
||||
return self._get_absolute_url()
|
||||
|
||||
|
||||
class CMSMenu(Menu):
|
||||
|
||||
def get_nodes(self, request):
|
||||
from cms.models import Title
|
||||
|
||||
site = self.renderer.site
|
||||
lang = self.renderer.request_language
|
||||
pages = get_page_queryset(
|
||||
site,
|
||||
draft=self.renderer.draft_mode_active,
|
||||
published=not self.renderer.draft_mode_active,
|
||||
)
|
||||
|
||||
if is_valid_site_language(lang, site_id=site.pk):
|
||||
_valid_language = True
|
||||
_hide_untranslated = hide_untranslated(lang, site.pk)
|
||||
else:
|
||||
_valid_language = False
|
||||
_hide_untranslated = False
|
||||
|
||||
if _valid_language:
|
||||
# The request language has been explicitly configured
|
||||
# for the current site.
|
||||
if _hide_untranslated:
|
||||
fallbacks = []
|
||||
else:
|
||||
fallbacks = get_fallback_languages(lang, site_id=site.pk)
|
||||
languages = [lang] + [_lang for _lang in fallbacks if _lang != lang]
|
||||
else:
|
||||
# The request language is not configured for the current site.
|
||||
# Fallback to all configured public languages for the current site.
|
||||
languages = get_public_languages(site.pk)
|
||||
fallbacks = languages
|
||||
|
||||
pages = (
|
||||
pages
|
||||
.filter(title_set__language__in=languages)
|
||||
.select_related('node')
|
||||
.order_by('node__path')
|
||||
.distinct()
|
||||
)
|
||||
|
||||
if not self.renderer.draft_mode_active:
|
||||
# we're dealing with public pages.
|
||||
# prefetch the draft versions.
|
||||
pages = pages.select_related('publisher_public__node')
|
||||
pages = get_visible_nodes(request, pages, site)
|
||||
|
||||
if not pages:
|
||||
return []
|
||||
|
||||
try:
|
||||
homepage = [page for page in pages if page.is_home][0]
|
||||
except IndexError:
|
||||
homepage = None
|
||||
|
||||
titles = Title.objects.filter(
|
||||
language__in=languages,
|
||||
publisher_is_draft=self.renderer.draft_mode_active,
|
||||
)
|
||||
|
||||
lookup = Prefetch(
|
||||
'title_set',
|
||||
to_attr='filtered_translations',
|
||||
queryset=titles,
|
||||
)
|
||||
prefetch_related_objects(pages, lookup)
|
||||
# Build the blank title instances only once
|
||||
blank_title_cache = {language: EmptyTitle(language=language) for language in languages}
|
||||
|
||||
if lang not in blank_title_cache:
|
||||
blank_title_cache[lang] = EmptyTitle(language=lang)
|
||||
|
||||
# Maps a node id to its page id
|
||||
node_id_to_page = {}
|
||||
|
||||
def _page_to_node(page):
|
||||
# EmptyTitle is used to prevent the cms from trying
|
||||
# to find a translation in the database
|
||||
page.title_cache = blank_title_cache.copy()
|
||||
|
||||
for trans in page.filtered_translations:
|
||||
page.title_cache[trans.language] = trans
|
||||
menu_node = get_menu_node_for_page(
|
||||
self.renderer,
|
||||
page,
|
||||
language=lang,
|
||||
fallbacks=fallbacks,
|
||||
)
|
||||
return menu_node
|
||||
|
||||
menu_nodes = []
|
||||
|
||||
for page in pages:
|
||||
node = page.node
|
||||
parent_id = node_id_to_page.get(node.parent_id)
|
||||
|
||||
if node.parent_id and not parent_id:
|
||||
# If the parent page is not available (unpublished, etc..)
|
||||
# don't bother creating menu nodes for its descendants.
|
||||
continue
|
||||
|
||||
menu_node = _page_to_node(page)
|
||||
cut_homepage = homepage and not homepage.in_navigation
|
||||
|
||||
if cut_homepage and parent_id == homepage.pk:
|
||||
# When the homepage is hidden from navigation,
|
||||
# we need to cut all its direct children from it.
|
||||
menu_node.parent_id = None
|
||||
else:
|
||||
menu_node.parent_id = parent_id
|
||||
node_id_to_page[node.pk] = page.pk
|
||||
menu_nodes.append(menu_node)
|
||||
return menu_nodes
|
||||
|
||||
|
||||
menu_pool.register_menu(CMSMenu)
|
||||
|
||||
|
||||
class NavExtender(Modifier):
|
||||
|
||||
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
|
||||
if post_cut:
|
||||
return nodes
|
||||
# rearrange the parent relations
|
||||
# Find home
|
||||
home = next((n for n in nodes if n.attr.get("is_home", False)), None)
|
||||
# Find nodes with NavExtenders
|
||||
exts = []
|
||||
for node in nodes:
|
||||
extenders = node.attr.get("navigation_extenders", None)
|
||||
if extenders:
|
||||
for ext in extenders:
|
||||
if ext not in exts:
|
||||
exts.append(ext)
|
||||
# Link the nodes
|
||||
for extnode in nodes:
|
||||
if extnode.namespace == ext and not extnode.parent_id:
|
||||
# if home has nav extenders but home is not visible
|
||||
if node == home and not node.visible:
|
||||
# extnode.parent_id = None
|
||||
extnode.parent_namespace = None
|
||||
extnode.parent = None
|
||||
else:
|
||||
extnode.parent_id = node.id
|
||||
extnode.parent_namespace = node.namespace
|
||||
extnode.parent = node
|
||||
node.children.append(extnode)
|
||||
removed = []
|
||||
|
||||
# find all not assigned nodes
|
||||
for menu in self.renderer.menus.items():
|
||||
if (hasattr(menu[1], 'cms_enabled')
|
||||
and menu[1].cms_enabled and not menu[0] in exts):
|
||||
for node in nodes:
|
||||
if node.namespace == menu[0]:
|
||||
removed.append(node)
|
||||
if breadcrumb:
|
||||
# if breadcrumb and home not in navigation add node
|
||||
if breadcrumb and home and not home.visible:
|
||||
home.visible = True
|
||||
if request.path_info == home.get_absolute_url():
|
||||
home.selected = True
|
||||
else:
|
||||
home.selected = False
|
||||
# remove all nodes that are nav_extenders and not assigned
|
||||
for node in removed:
|
||||
nodes.remove(node)
|
||||
return nodes
|
||||
|
||||
menu_pool.register_modifier(NavExtender)
|
||||
|
||||
|
||||
class SoftRootCutter(Modifier):
|
||||
"""
|
||||
Ask evildmp/superdmp if you don't understand softroots!
|
||||
|
||||
Softroot description from the docs:
|
||||
|
||||
A soft root is a page that acts as the root for a menu navigation tree.
|
||||
|
||||
Typically, this will be a page that is the root of a significant new
|
||||
section on your site.
|
||||
|
||||
When the soft root feature is enabled, the navigation menu for any page
|
||||
will start at the nearest soft root, rather than at the real root of
|
||||
the site’s page hierarchy.
|
||||
|
||||
This feature is useful when your site has deep page hierarchies (and
|
||||
therefore multiple levels in its navigation trees). In such a case, you
|
||||
usually don’t want to present site visitors with deep menus of nested
|
||||
items.
|
||||
|
||||
For example, you’re on the page -Introduction to Bleeding-?, so the menu
|
||||
might look like this:
|
||||
|
||||
School of Medicine
|
||||
Medical Education
|
||||
Departments
|
||||
Department of Lorem Ipsum
|
||||
Department of Donec Imperdiet
|
||||
Department of Cras Eros
|
||||
Department of Mediaeval Surgery
|
||||
Theory
|
||||
Cures
|
||||
Bleeding
|
||||
Introduction to Bleeding <this is the current page>
|
||||
Bleeding - the scientific evidence
|
||||
Cleaning up the mess
|
||||
Cupping
|
||||
Leaches
|
||||
Maggots
|
||||
Techniques
|
||||
Instruments
|
||||
Department of Curabitur a Purus
|
||||
Department of Sed Accumsan
|
||||
Department of Etiam
|
||||
Research
|
||||
Administration
|
||||
Contact us
|
||||
Impressum
|
||||
|
||||
which is frankly overwhelming.
|
||||
|
||||
By making -Department of Mediaeval Surgery-? a soft root, the menu
|
||||
becomes much more manageable:
|
||||
|
||||
Department of Mediaeval Surgery
|
||||
Theory
|
||||
Cures
|
||||
Bleeding
|
||||
Introduction to Bleeding <current page>
|
||||
Bleeding - the scientific evidence
|
||||
Cleaning up the mess
|
||||
Cupping
|
||||
Leaches
|
||||
Maggots
|
||||
Techniques
|
||||
Instruments
|
||||
"""
|
||||
|
||||
def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb):
|
||||
# only apply this modifier if we're pre-cut (since what we do is cut)
|
||||
# or if no id argument is provided, indicating {% show_menu_below_id %}
|
||||
if post_cut or root_id:
|
||||
return nodes
|
||||
selected = None
|
||||
root_nodes = []
|
||||
# find the selected node as well as all the root nodes
|
||||
for node in nodes:
|
||||
if node.selected:
|
||||
selected = node
|
||||
if not node.parent:
|
||||
root_nodes.append(node)
|
||||
|
||||
# if we found a selected ...
|
||||
if selected:
|
||||
# and the selected is a softroot
|
||||
if selected.attr.get("soft_root", False):
|
||||
# get it's descendants
|
||||
nodes = selected.get_descendants()
|
||||
# remove the link to parent
|
||||
selected.parent = None
|
||||
# make the selected page the root in the menu
|
||||
nodes = [selected] + nodes
|
||||
else:
|
||||
# if it's not a soft root, walk ancestors (upwards!)
|
||||
nodes = self.find_ancestors_and_remove_children(selected, nodes)
|
||||
return nodes
|
||||
|
||||
def find_and_remove_children(self, node, nodes):
|
||||
for child in node.children:
|
||||
if child.attr.get("soft_root", False):
|
||||
self.remove_children(child, nodes)
|
||||
return nodes
|
||||
|
||||
def remove_children(self, node, nodes):
|
||||
for child in node.children:
|
||||
nodes.remove(child)
|
||||
self.remove_children(child, nodes)
|
||||
node.children = []
|
||||
|
||||
def find_ancestors_and_remove_children(self, node, nodes):
|
||||
"""
|
||||
Check ancestors of node for soft roots
|
||||
"""
|
||||
if node.parent:
|
||||
if node.parent.attr.get("soft_root", False):
|
||||
nodes = node.parent.get_descendants()
|
||||
node.parent.parent = None
|
||||
nodes = [node.parent] + nodes
|
||||
else:
|
||||
nodes = self.find_ancestors_and_remove_children(
|
||||
node.parent, nodes)
|
||||
else:
|
||||
for newnode in nodes:
|
||||
if newnode != node and not newnode.parent:
|
||||
self.find_and_remove_children(newnode, nodes)
|
||||
for child in node.children:
|
||||
if child != node:
|
||||
self.find_and_remove_children(child, nodes)
|
||||
return nodes
|
||||
|
||||
|
||||
menu_pool.register_modifier(SoftRootCutter)
|
||||
@@ -1,137 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from cms.models import CMSPlugin, Placeholder
|
||||
from cms.models.aliaspluginmodel import AliasPluginModel
|
||||
from cms.models.placeholderpluginmodel import PlaceholderReference
|
||||
from cms.plugin_base import CMSPluginBase, PluginMenuItem
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils.urlutils import admin_reverse
|
||||
from django.conf.urls import url
|
||||
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponse
|
||||
from django.middleware.csrf import get_token
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _, get_language
|
||||
|
||||
|
||||
class PlaceholderPlugin(CMSPluginBase):
|
||||
name = _("Placeholder")
|
||||
parent_classes = ['0'] # so you will not be able to add it something
|
||||
#require_parent = True
|
||||
render_plugin = False
|
||||
admin_preview = False
|
||||
system = True
|
||||
|
||||
model = PlaceholderReference
|
||||
|
||||
|
||||
plugin_pool.register_plugin(PlaceholderPlugin)
|
||||
|
||||
|
||||
class AliasPlugin(CMSPluginBase):
|
||||
name = _("Alias")
|
||||
allow_children = False
|
||||
model = AliasPluginModel
|
||||
render_template = "cms/plugins/alias.html"
|
||||
system = True
|
||||
|
||||
@classmethod
|
||||
def get_render_queryset(cls):
|
||||
queryset = super(AliasPlugin, cls).get_render_queryset()
|
||||
return queryset.select_related('plugin', 'alias_placeholder')
|
||||
|
||||
@classmethod
|
||||
def get_extra_plugin_menu_items(cls, request, plugin):
|
||||
return [
|
||||
PluginMenuItem(
|
||||
_("Create Alias"),
|
||||
admin_reverse("cms_create_alias"),
|
||||
data={'plugin_id': plugin.pk, 'csrfmiddlewaretoken': get_token(request)},
|
||||
)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_extra_placeholder_menu_items(cls, request, placeholder):
|
||||
return [
|
||||
PluginMenuItem(
|
||||
_("Create Alias"),
|
||||
admin_reverse("cms_create_alias"),
|
||||
data={'placeholder_id': placeholder.pk, 'csrfmiddlewaretoken': get_token(request)},
|
||||
)
|
||||
]
|
||||
|
||||
def get_plugin_urls(self):
|
||||
return [
|
||||
url(r'^create_alias/$', self.create_alias, name='cms_create_alias'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_empty_change_form_text(cls, obj=None):
|
||||
original = super(AliasPlugin, cls).get_empty_change_form_text(obj=obj)
|
||||
|
||||
if not obj:
|
||||
return original
|
||||
|
||||
instance = obj.get_plugin_instance()[0]
|
||||
|
||||
if not instance:
|
||||
# Ghost plugin
|
||||
return original
|
||||
|
||||
aliased_placeholder_id = instance.get_aliased_placeholder_id()
|
||||
|
||||
if not aliased_placeholder_id:
|
||||
# Corrupt (sadly) Alias plugin
|
||||
return original
|
||||
|
||||
aliased_placeholder = Placeholder.objects.get(pk=aliased_placeholder_id)
|
||||
|
||||
origin_page = aliased_placeholder.page
|
||||
|
||||
if not origin_page:
|
||||
# Placeholder is not attached to a page
|
||||
return original
|
||||
|
||||
# I have a feeling this could fail with a NoReverseMatch error
|
||||
# if this is the case, then it's likely a corruption.
|
||||
page_url = origin_page.get_absolute_url(language=obj.language)
|
||||
page_title = origin_page.get_title(language=obj.language)
|
||||
|
||||
message = ugettext('This is an alias reference, '
|
||||
'you can edit the content only on the '
|
||||
'<a href="%(page_url)s?edit" target="_parent">%(page_title)s</a> page.')
|
||||
return message % {'page_url': page_url, 'page_title': page_title}
|
||||
|
||||
def create_alias(self, request):
|
||||
if not request.user.is_staff:
|
||||
return HttpResponseForbidden("not enough privileges")
|
||||
if not 'plugin_id' in request.POST and not 'placeholder_id' in request.POST:
|
||||
return HttpResponseBadRequest("plugin_id or placeholder_id POST parameter missing.")
|
||||
plugin = None
|
||||
placeholder = None
|
||||
if 'plugin_id' in request.POST:
|
||||
pk = request.POST['plugin_id']
|
||||
try:
|
||||
plugin = CMSPlugin.objects.get(pk=pk)
|
||||
except CMSPlugin.DoesNotExist:
|
||||
return HttpResponseBadRequest("plugin with id %s not found." % pk)
|
||||
if 'placeholder_id' in request.POST:
|
||||
pk = request.POST['placeholder_id']
|
||||
try:
|
||||
placeholder = Placeholder.objects.get(pk=pk)
|
||||
except Placeholder.DoesNotExist:
|
||||
return HttpResponseBadRequest("placeholder with id %s not found." % pk)
|
||||
if not placeholder.has_change_permission(request.user):
|
||||
return HttpResponseBadRequest("You do not have enough permission to alias this placeholder.")
|
||||
clipboard = request.toolbar.clipboard
|
||||
clipboard.cmsplugin_set.all().delete()
|
||||
language = get_language()
|
||||
if plugin:
|
||||
language = plugin.language
|
||||
alias = AliasPluginModel(language=language, placeholder=clipboard, plugin_type="AliasPlugin")
|
||||
if plugin:
|
||||
alias.plugin = plugin
|
||||
if placeholder:
|
||||
alias.alias_placeholder = placeholder
|
||||
alias.save()
|
||||
return HttpResponse("ok")
|
||||
|
||||
|
||||
plugin_pool.register_plugin(AliasPlugin)
|
||||
@@ -1,749 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_permission_codename, get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db.models import Q
|
||||
from django.urls import NoReverseMatch, Resolver404, resolve, reverse
|
||||
from django.utils.translation import override as force_language, ugettext_lazy as _
|
||||
|
||||
from cms.api import get_page_draft, can_change_page
|
||||
from cms.constants import TEMPLATE_INHERITANCE_MAGIC, PUBLISHER_STATE_PENDING
|
||||
from cms.models import Placeholder, Title, Page, PageType, StaticPlaceholder
|
||||
from cms.toolbar.items import ButtonList, TemplateItem, REFRESH_PAGE
|
||||
from cms.toolbar_base import CMSToolbar
|
||||
from cms.toolbar_pool import toolbar_pool
|
||||
from cms.utils import get_language_from_request, page_permissions
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.i18n import get_language_tuple, get_language_dict
|
||||
from cms.utils.page_permissions import (
|
||||
user_can_change_page,
|
||||
user_can_delete_page,
|
||||
user_can_publish_page,
|
||||
)
|
||||
from cms.utils.urlutils import add_url_parameters, admin_reverse
|
||||
|
||||
from menus.utils import DefaultLanguageChanger
|
||||
|
||||
|
||||
# Identifiers for search
|
||||
ADMIN_MENU_IDENTIFIER = 'admin-menu'
|
||||
LANGUAGE_MENU_IDENTIFIER = 'language-menu'
|
||||
TEMPLATE_MENU_BREAK = 'Template Menu Break'
|
||||
PAGE_MENU_IDENTIFIER = 'page'
|
||||
PAGE_MENU_ADD_IDENTIFIER = 'add_page'
|
||||
PAGE_MENU_FIRST_BREAK = 'Page Menu First Break'
|
||||
PAGE_MENU_SECOND_BREAK = 'Page Menu Second Break'
|
||||
PAGE_MENU_THIRD_BREAK = 'Page Menu Third Break'
|
||||
PAGE_MENU_FOURTH_BREAK = 'Page Menu Fourth Break'
|
||||
PAGE_MENU_LAST_BREAK = 'Page Menu Last Break'
|
||||
HISTORY_MENU_BREAK = 'History Menu Break'
|
||||
MANAGE_PAGES_BREAK = 'Manage Pages Break'
|
||||
ADMIN_SITES_BREAK = 'Admin Sites Break'
|
||||
ADMINISTRATION_BREAK = 'Administration Break'
|
||||
CLIPBOARD_BREAK = 'Clipboard Break'
|
||||
USER_SETTINGS_BREAK = 'User Settings Break'
|
||||
ADD_PAGE_LANGUAGE_BREAK = "Add page language Break"
|
||||
REMOVE_PAGE_LANGUAGE_BREAK = "Remove page language Break"
|
||||
COPY_PAGE_LANGUAGE_BREAK = "Copy page language Break"
|
||||
TOOLBAR_DISABLE_BREAK = 'Toolbar disable Break'
|
||||
SHORTCUTS_BREAK = 'Shortcuts Break'
|
||||
|
||||
|
||||
@toolbar_pool.register
|
||||
class PlaceholderToolbar(CMSToolbar):
|
||||
"""
|
||||
Adds placeholder edit buttons if placeholders or static placeholders are detected in the template
|
||||
"""
|
||||
|
||||
def populate(self):
|
||||
self.page = get_page_draft(self.request.current_page)
|
||||
|
||||
def post_template_populate(self):
|
||||
super(PlaceholderToolbar, self).post_template_populate()
|
||||
self.add_wizard_button()
|
||||
|
||||
def add_wizard_button(self):
|
||||
from cms.wizards.wizard_pool import entry_choices
|
||||
title = _("Create")
|
||||
|
||||
if self.page:
|
||||
user = self.request.user
|
||||
page_pk = self.page.pk
|
||||
disabled = len(list(entry_choices(user, self.page))) == 0
|
||||
else:
|
||||
page_pk = ''
|
||||
disabled = True
|
||||
|
||||
url = '{url}?page={page}&language={lang}&edit'.format(
|
||||
url=reverse("cms_wizard_create"),
|
||||
page=page_pk,
|
||||
lang=self.toolbar.site_language,
|
||||
)
|
||||
self.toolbar.add_modal_button(title, url,
|
||||
side=self.toolbar.RIGHT,
|
||||
disabled=disabled,
|
||||
on_close=REFRESH_PAGE)
|
||||
|
||||
|
||||
@toolbar_pool.register
|
||||
class BasicToolbar(CMSToolbar):
|
||||
"""
|
||||
Basic Toolbar for site and languages menu
|
||||
"""
|
||||
page = None
|
||||
_language_menu = None
|
||||
_admin_menu = None
|
||||
|
||||
def init_from_request(self):
|
||||
self.page = get_page_draft(self.request.current_page)
|
||||
|
||||
def populate(self):
|
||||
if not self.page:
|
||||
self.init_from_request()
|
||||
self.clipboard = self.request.toolbar.user_settings.clipboard
|
||||
self.add_admin_menu()
|
||||
self.add_language_menu()
|
||||
|
||||
def add_admin_menu(self):
|
||||
if not self._admin_menu:
|
||||
self._admin_menu = self.toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER, self.current_site.name)
|
||||
# Users button
|
||||
self.add_users_button(self._admin_menu)
|
||||
|
||||
# sites menu
|
||||
sites_queryset = Site.objects.order_by('name')
|
||||
|
||||
if len(sites_queryset) > 1:
|
||||
sites_menu = self._admin_menu.get_or_create_menu('sites', _('Sites'))
|
||||
sites_menu.add_sideframe_item(_('Admin Sites'), url=admin_reverse('sites_site_changelist'))
|
||||
sites_menu.add_break(ADMIN_SITES_BREAK)
|
||||
for site in sites_queryset:
|
||||
sites_menu.add_link_item(site.name, url='http://%s' % site.domain,
|
||||
active=site.pk == self.current_site.pk)
|
||||
|
||||
# admin
|
||||
self._admin_menu.add_sideframe_item(_('Administration'), url=admin_reverse('index'))
|
||||
self._admin_menu.add_break(ADMINISTRATION_BREAK)
|
||||
|
||||
# cms users settings
|
||||
self._admin_menu.add_sideframe_item(_('User settings'), url=admin_reverse('cms_usersettings_change'))
|
||||
self._admin_menu.add_break(USER_SETTINGS_BREAK)
|
||||
|
||||
# clipboard
|
||||
if self.toolbar.edit_mode_active:
|
||||
# True if the clipboard exists and there's plugins in it.
|
||||
clipboard_is_bound = self.toolbar.clipboard_plugin
|
||||
|
||||
self._admin_menu.add_link_item(_('Clipboard...'), url='#',
|
||||
extra_classes=['cms-clipboard-trigger'],
|
||||
disabled=not clipboard_is_bound)
|
||||
self._admin_menu.add_link_item(_('Clear clipboard'), url='#',
|
||||
extra_classes=['cms-clipboard-empty'],
|
||||
disabled=not clipboard_is_bound)
|
||||
self._admin_menu.add_break(CLIPBOARD_BREAK)
|
||||
|
||||
# Disable toolbar
|
||||
self._admin_menu.add_link_item(_('Disable toolbar'), url='?%s' % get_cms_setting('CMS_TOOLBAR_URL__DISABLE'))
|
||||
self._admin_menu.add_break(TOOLBAR_DISABLE_BREAK)
|
||||
self._admin_menu.add_link_item(_('Shortcuts...'), url='#',
|
||||
extra_classes=('cms-show-shortcuts',))
|
||||
self._admin_menu.add_break(SHORTCUTS_BREAK)
|
||||
|
||||
# logout
|
||||
self.add_logout_button(self._admin_menu)
|
||||
|
||||
def add_users_button(self, parent):
|
||||
User = get_user_model()
|
||||
|
||||
if User in admin.site._registry:
|
||||
opts = User._meta
|
||||
|
||||
if self.request.user.has_perm('%s.%s' % (opts.app_label, get_permission_codename('change', opts))):
|
||||
user_changelist_url = admin_reverse('%s_%s_changelist' % (opts.app_label, opts.model_name))
|
||||
parent.add_sideframe_item(_('Users'), url=user_changelist_url)
|
||||
|
||||
def add_logout_button(self, parent):
|
||||
# If current page is not published or has view restrictions user is redirected to the home page:
|
||||
# * published page: no redirect
|
||||
# * unpublished page: redirect to the home page
|
||||
# * published page with login_required: redirect to the home page
|
||||
# * published page with view permissions: redirect to the home page
|
||||
page_is_published = self.page and self.page.is_published(self.current_lang)
|
||||
|
||||
if page_is_published and not self.page.login_required:
|
||||
anon_can_access = page_permissions.user_can_view_page(
|
||||
user=AnonymousUser(),
|
||||
page=self.page,
|
||||
site=self.current_site,
|
||||
)
|
||||
else:
|
||||
anon_can_access = False
|
||||
|
||||
on_success = self.toolbar.REFRESH_PAGE if anon_can_access else '/'
|
||||
|
||||
# We'll show "Logout Joe Bloggs" if the name fields in auth.User are completed, else "Logout jbloggs". If
|
||||
# anything goes wrong, it'll just be "Logout".
|
||||
|
||||
user_name = self.get_username()
|
||||
logout_menu_text = _('Logout %s') % user_name if user_name else _('Logout')
|
||||
|
||||
parent.add_ajax_item(
|
||||
logout_menu_text,
|
||||
action=admin_reverse('logout'),
|
||||
active=True,
|
||||
on_success=on_success,
|
||||
method='GET',
|
||||
)
|
||||
|
||||
def add_language_menu(self):
|
||||
if settings.USE_I18N and not self._language_menu:
|
||||
self._language_menu = self.toolbar.get_or_create_menu(LANGUAGE_MENU_IDENTIFIER, _('Language'), position=-1)
|
||||
language_changer = getattr(self.request, '_language_changer', DefaultLanguageChanger(self.request))
|
||||
for code, name in get_language_tuple(self.current_site.pk):
|
||||
try:
|
||||
url = language_changer(code)
|
||||
except NoReverseMatch:
|
||||
url = DefaultLanguageChanger(self.request)(code)
|
||||
self._language_menu.add_link_item(name, url=url, active=self.current_lang == code)
|
||||
|
||||
def get_username(self, user=None, default=''):
|
||||
user = user or self.request.user
|
||||
try:
|
||||
name = user.get_full_name()
|
||||
if name:
|
||||
return name
|
||||
else:
|
||||
return user.get_username()
|
||||
except (AttributeError, NotImplementedError):
|
||||
return default
|
||||
|
||||
|
||||
@toolbar_pool.register
|
||||
class PageToolbar(CMSToolbar):
|
||||
_changed_admin_menu = None
|
||||
watch_models = [Page, PageType]
|
||||
|
||||
def init_placeholders(self):
|
||||
request = self.request
|
||||
toolbar = self.toolbar
|
||||
|
||||
if toolbar._async and 'placeholders[]' in request.GET:
|
||||
# AJAX request to reload page structure
|
||||
placeholder_ids = request.GET.getlist("placeholders[]")
|
||||
self.placeholders = Placeholder.objects.filter(pk__in=placeholder_ids)
|
||||
self.statics = StaticPlaceholder.objects.filter(
|
||||
Q(draft__in=placeholder_ids) | Q(public__in=placeholder_ids)
|
||||
)
|
||||
self.dirty_statics = [sp for sp in self.statics if sp.dirty]
|
||||
else:
|
||||
if toolbar.structure_mode_active and not toolbar.uses_legacy_structure_mode:
|
||||
# User has explicitly requested structure mode
|
||||
# and the object (page, blog, etc..) allows for the non-legacy structure mode
|
||||
renderer = toolbar.structure_renderer
|
||||
else:
|
||||
renderer = toolbar.get_content_renderer()
|
||||
|
||||
self.placeholders = renderer.get_rendered_placeholders()
|
||||
self.statics = renderer.get_rendered_static_placeholders()
|
||||
self.dirty_statics = [sp for sp in self.statics if sp.dirty]
|
||||
|
||||
def add_structure_mode(self):
|
||||
if self.page and not self.page.application_urls:
|
||||
if user_can_change_page(self.request.user, page=self.page):
|
||||
return self.add_structure_mode_item()
|
||||
|
||||
elif any(ph for ph in self.placeholders if ph.has_change_permission(self.request.user)):
|
||||
return self.add_structure_mode_item()
|
||||
|
||||
for sp in self.statics:
|
||||
if sp.has_change_permission(self.request):
|
||||
return self.add_structure_mode_item()
|
||||
|
||||
def add_structure_mode_item(self, extra_classes=('cms-toolbar-item-cms-mode-switcher',)):
|
||||
structure_active = self.toolbar.structure_mode_active
|
||||
edit_mode_active = (not structure_active and self.toolbar.edit_mode_active)
|
||||
build_url = '{}?{}'.format(self.toolbar.request_path, get_cms_setting('CMS_TOOLBAR_URL__BUILD'))
|
||||
edit_url = '{}?{}'.format(self.toolbar.request_path, get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON'))
|
||||
|
||||
if self.request.user.has_perm("cms.use_structure"):
|
||||
switcher = self.toolbar.add_button_list('Mode Switcher', side=self.toolbar.RIGHT,
|
||||
extra_classes=extra_classes)
|
||||
switcher.add_button(_('Structure'), build_url, active=structure_active, disabled=False,
|
||||
extra_classes='cms-structure-btn')
|
||||
switcher.add_button(_('Content'), edit_url, active=edit_mode_active, disabled=False,
|
||||
extra_classes='cms-content-btn')
|
||||
|
||||
def get_title(self):
|
||||
try:
|
||||
return Title.objects.get(page=self.page, language=self.current_lang, publisher_is_draft=True)
|
||||
except Title.DoesNotExist:
|
||||
return None
|
||||
|
||||
def has_publish_permission(self):
|
||||
if self.page:
|
||||
publish_permission = page_permissions.user_can_publish_page(
|
||||
self.request.user,
|
||||
page=self.page,
|
||||
site=self.current_site
|
||||
)
|
||||
else:
|
||||
publish_permission = False
|
||||
|
||||
if publish_permission and self.statics:
|
||||
publish_permission = all(sp.has_publish_permission(self.request) for sp in self.dirty_statics)
|
||||
return publish_permission
|
||||
|
||||
def has_unpublish_permission(self):
|
||||
return self.has_publish_permission()
|
||||
|
||||
def has_page_change_permission(self):
|
||||
if not hasattr(self, 'page_change_permission'):
|
||||
self.page_change_permission = can_change_page(self.request)
|
||||
return self.page_change_permission
|
||||
|
||||
def page_is_pending(self, page, language):
|
||||
return (page.publisher_public_id and
|
||||
page.publisher_public.get_publisher_state(language) == PUBLISHER_STATE_PENDING)
|
||||
|
||||
def in_apphook(self):
|
||||
with force_language(self.toolbar.request_language):
|
||||
try:
|
||||
resolver = resolve(self.toolbar.request_path)
|
||||
except Resolver404:
|
||||
return False
|
||||
else:
|
||||
from cms.views import details
|
||||
return resolver.func != details
|
||||
|
||||
def in_apphook_root(self):
|
||||
"""
|
||||
Returns True if the request is for a page handled by an apphook, but
|
||||
is also the page it is attached to.
|
||||
:return: Boolean
|
||||
"""
|
||||
page = getattr(self.request, 'current_page', False)
|
||||
if page:
|
||||
language = get_language_from_request(self.request)
|
||||
return self.toolbar.request_path == page.get_absolute_url(language=language)
|
||||
return False
|
||||
|
||||
def get_on_delete_redirect_url(self):
|
||||
language = self.current_lang
|
||||
parent_page = self.page.parent_page if self.page else None
|
||||
|
||||
# if the current page has a parent in the request's current language redirect to it
|
||||
if parent_page and language in parent_page.get_languages():
|
||||
with force_language(language):
|
||||
return parent_page.get_absolute_url(language=language)
|
||||
|
||||
# else redirect to root, do not redirect to Page.objects.get_home() because user could have deleted the last
|
||||
# page, if DEBUG == False this could cause a 404
|
||||
return reverse('pages-root')
|
||||
|
||||
# Populate
|
||||
|
||||
def populate(self):
|
||||
self.page = get_page_draft(self.request.current_page)
|
||||
self.title = self.get_title()
|
||||
self.permissions_activated = get_cms_setting('PERMISSION')
|
||||
self.change_admin_menu()
|
||||
self.add_page_menu()
|
||||
self.change_language_menu()
|
||||
|
||||
def post_template_populate(self):
|
||||
self.init_placeholders()
|
||||
self.add_draft_live()
|
||||
self.add_publish_button()
|
||||
self.add_structure_mode()
|
||||
|
||||
def has_dirty_objects(self):
|
||||
language = self.current_lang
|
||||
|
||||
if self.page:
|
||||
if self.dirty_statics:
|
||||
# There's dirty static placeholders on this page.
|
||||
# Only show the page as dirty (publish button) if the page
|
||||
# translation has been configured.
|
||||
dirty = self.page.has_translation(language)
|
||||
else:
|
||||
dirty = (self.page.is_dirty(language) or self.page_is_pending(self.page, language))
|
||||
else:
|
||||
dirty = bool(self.dirty_statics)
|
||||
return dirty
|
||||
|
||||
# Buttons
|
||||
|
||||
def add_publish_button(self, classes=('cms-btn-action', 'cms-btn-publish',)):
|
||||
if self.user_can_publish():
|
||||
button = self.get_publish_button(classes=classes)
|
||||
self.toolbar.add_item(button)
|
||||
|
||||
def user_can_publish(self):
|
||||
if self.page and self.page.is_page_type:
|
||||
# By design, page-types are not publishable.
|
||||
return False
|
||||
|
||||
if not self.toolbar.edit_mode_active:
|
||||
return False
|
||||
return self.has_publish_permission() and self.has_dirty_objects()
|
||||
|
||||
def get_publish_button(self, classes=None):
|
||||
dirty = self.has_dirty_objects()
|
||||
classes = list(classes or [])
|
||||
|
||||
if dirty and 'cms-btn-publish-active' not in classes:
|
||||
classes.append('cms-btn-publish-active')
|
||||
|
||||
if self.dirty_statics or (self.page and self.page.is_published(self.current_lang)):
|
||||
title = _('Publish page changes')
|
||||
else:
|
||||
title = _('Publish page now')
|
||||
classes.append('cms-publish-page')
|
||||
|
||||
item = ButtonList(side=self.toolbar.RIGHT)
|
||||
item.add_button(
|
||||
title,
|
||||
url=self.get_publish_url(),
|
||||
disabled=not dirty,
|
||||
extra_classes=classes,
|
||||
)
|
||||
return item
|
||||
|
||||
def get_publish_url(self):
|
||||
pk = self.page.pk if self.page else 0
|
||||
params = {}
|
||||
|
||||
if self.dirty_statics:
|
||||
params['statics'] = ','.join(str(sp.pk) for sp in self.dirty_statics)
|
||||
|
||||
if self.in_apphook():
|
||||
params['redirect'] = self.toolbar.request_path
|
||||
|
||||
with force_language(self.current_lang):
|
||||
url = admin_reverse('cms_page_publish_page', args=(pk, self.current_lang))
|
||||
return add_url_parameters(url, params)
|
||||
|
||||
def add_draft_live(self):
|
||||
if self.page:
|
||||
if self.toolbar.edit_mode_active and not self.title:
|
||||
self.add_page_settings_button()
|
||||
|
||||
if user_can_change_page(self.request.user, page=self.page) and self.page.is_published(self.current_lang):
|
||||
return self.add_draft_live_item()
|
||||
|
||||
elif self.placeholders:
|
||||
return self.add_draft_live_item()
|
||||
|
||||
for sp in self.statics:
|
||||
if sp.has_change_permission(self.request):
|
||||
return self.add_draft_live_item()
|
||||
|
||||
def add_draft_live_item(self, template='cms/toolbar/items/live_draft.html', extra_context=None):
|
||||
context = {'cms_toolbar': self.toolbar}
|
||||
context.update(extra_context or {})
|
||||
pos = len(self.toolbar.right_items)
|
||||
self.toolbar.add_item(TemplateItem(template, extra_context=context, side=self.toolbar.RIGHT), position=pos)
|
||||
|
||||
def add_page_settings_button(self, extra_classes=('cms-btn-action',)):
|
||||
url = '%s?language=%s' % (admin_reverse('cms_page_change', args=[self.page.pk]), self.toolbar.request_language)
|
||||
self.toolbar.add_modal_button(_('Page settings'), url, side=self.toolbar.RIGHT, extra_classes=extra_classes)
|
||||
|
||||
# Menus
|
||||
def change_language_menu(self):
|
||||
if self.toolbar.edit_mode_active and self.page:
|
||||
can_change = page_permissions.user_can_change_page(
|
||||
user=self.request.user,
|
||||
page=self.page,
|
||||
site=self.current_site,
|
||||
)
|
||||
else:
|
||||
can_change = False
|
||||
|
||||
if can_change:
|
||||
language_menu = self.toolbar.get_menu(LANGUAGE_MENU_IDENTIFIER)
|
||||
if not language_menu:
|
||||
return None
|
||||
|
||||
languages = get_language_dict(self.current_site.pk)
|
||||
|
||||
remove = [(code, languages.get(code, code)) for code in self.page.get_languages() if code in languages]
|
||||
add = [l for l in languages.items() if l not in remove]
|
||||
copy = [(code, name) for code, name in languages.items() if code != self.current_lang and (code, name) in remove]
|
||||
|
||||
if add or remove or copy:
|
||||
language_menu.add_break(ADD_PAGE_LANGUAGE_BREAK)
|
||||
|
||||
if add:
|
||||
add_plugins_menu = language_menu.get_or_create_menu('{0}-add'.format(LANGUAGE_MENU_IDENTIFIER), _('Add Translation'))
|
||||
|
||||
if self.page.is_page_type:
|
||||
page_change_url = admin_reverse('cms_pagetype_change', args=(self.page.pk,))
|
||||
else:
|
||||
page_change_url = admin_reverse('cms_page_change', args=(self.page.pk,))
|
||||
|
||||
for code, name in add:
|
||||
url = add_url_parameters(page_change_url, language=code)
|
||||
add_plugins_menu.add_modal_item(name, url=url)
|
||||
|
||||
if remove:
|
||||
if self.page.is_page_type:
|
||||
translation_delete_url = admin_reverse('cms_pagetype_delete_translation', args=(self.page.pk,))
|
||||
else:
|
||||
translation_delete_url = admin_reverse('cms_page_delete_translation', args=(self.page.pk,))
|
||||
|
||||
remove_plugins_menu = language_menu.get_or_create_menu('{0}-del'.format(LANGUAGE_MENU_IDENTIFIER), _('Delete Translation'))
|
||||
disabled = len(remove) == 1
|
||||
for code, name in remove:
|
||||
url = add_url_parameters(translation_delete_url, language=code)
|
||||
remove_plugins_menu.add_modal_item(name, url=url, disabled=disabled)
|
||||
|
||||
if copy:
|
||||
copy_plugins_menu = language_menu.get_or_create_menu('{0}-copy'.format(LANGUAGE_MENU_IDENTIFIER), _('Copy all plugins'))
|
||||
title = _('from %s')
|
||||
question = _('Are you sure you want to copy all plugins from %s?')
|
||||
|
||||
if self.page.is_page_type:
|
||||
page_copy_url = admin_reverse('cms_pagetype_copy_language', args=(self.page.pk,))
|
||||
else:
|
||||
page_copy_url = admin_reverse('cms_page_copy_language', args=(self.page.pk,))
|
||||
|
||||
for code, name in copy:
|
||||
copy_plugins_menu.add_ajax_item(
|
||||
title % name, action=page_copy_url,
|
||||
data={'source_language': code, 'target_language': self.current_lang},
|
||||
question=question % name, on_success=self.toolbar.REFRESH_PAGE
|
||||
)
|
||||
|
||||
def change_admin_menu(self):
|
||||
can_change_page = self.has_page_change_permission()
|
||||
|
||||
if not can_change_page:
|
||||
# Check if the user has permissions to change at least one page
|
||||
can_change_page = page_permissions.user_can_change_at_least_one_page(
|
||||
user=self.request.user,
|
||||
site=self.current_site,
|
||||
)
|
||||
|
||||
if not self._changed_admin_menu and can_change_page:
|
||||
admin_menu = self.toolbar.get_or_create_menu(ADMIN_MENU_IDENTIFIER)
|
||||
url = admin_reverse('cms_page_changelist') # cms page admin
|
||||
params = {'language': self.toolbar.request_language}
|
||||
if self.page:
|
||||
params['page_id'] = self.page.pk
|
||||
url = add_url_parameters(url, params)
|
||||
admin_menu.add_sideframe_item(_('Pages'), url=url, position=0)
|
||||
# Used to prevent duplicates
|
||||
self._changed_admin_menu = True
|
||||
|
||||
def add_page_menu(self):
|
||||
if self.page:
|
||||
edit_mode = self.toolbar.edit_mode_active
|
||||
refresh = self.toolbar.REFRESH_PAGE
|
||||
can_change = user_can_change_page(
|
||||
user=self.request.user,
|
||||
page=self.page,
|
||||
site=self.current_site,
|
||||
)
|
||||
|
||||
# menu for current page
|
||||
# NOTE: disabled if the current path is "deeper" into the
|
||||
# application's url patterns than its root. This is because
|
||||
# when the Content Manager is at the root of the app-hook,
|
||||
# some of the page options still make sense.
|
||||
current_page_menu = self.toolbar.get_or_create_menu(
|
||||
PAGE_MENU_IDENTIFIER, _('Page'), position=1, disabled=self.in_apphook() and not self.in_apphook_root())
|
||||
|
||||
new_page_params = {'edit': 1}
|
||||
new_sub_page_params = {'edit': 1, 'parent_node': self.page.node_id}
|
||||
|
||||
if self.page.is_page_type:
|
||||
add_page_url = admin_reverse('cms_pagetype_add')
|
||||
advanced_url = admin_reverse('cms_pagetype_advanced', args=(self.page.pk,))
|
||||
page_settings_url = admin_reverse('cms_pagetype_change', args=(self.page.pk,))
|
||||
duplicate_page_url = admin_reverse('cms_pagetype_duplicate', args=[self.page.pk])
|
||||
else:
|
||||
add_page_url = admin_reverse('cms_page_add')
|
||||
advanced_url = admin_reverse('cms_page_advanced', args=(self.page.pk,))
|
||||
page_settings_url = admin_reverse('cms_page_change', args=(self.page.pk,))
|
||||
duplicate_page_url = admin_reverse('cms_page_duplicate', args=[self.page.pk])
|
||||
|
||||
can_add_root_page = page_permissions.user_can_add_page(
|
||||
user=self.request.user,
|
||||
site=self.current_site,
|
||||
)
|
||||
|
||||
if self.page.parent_page:
|
||||
new_page_params['parent_node'] = self.page.parent_page.node_id
|
||||
can_add_sibling_page = page_permissions.user_can_add_subpage(
|
||||
user=self.request.user,
|
||||
target=self.page.parent_page,
|
||||
)
|
||||
else:
|
||||
can_add_sibling_page = can_add_root_page
|
||||
|
||||
can_add_sub_page = page_permissions.user_can_add_subpage(
|
||||
user=self.request.user,
|
||||
target=self.page,
|
||||
)
|
||||
|
||||
# page operations menu
|
||||
add_page_menu = current_page_menu.get_or_create_menu(
|
||||
PAGE_MENU_ADD_IDENTIFIER,
|
||||
_('Create Page'),
|
||||
)
|
||||
|
||||
add_page_menu_modal_items = (
|
||||
(_('New Page'), new_page_params, can_add_sibling_page),
|
||||
(_('New Sub Page'), new_sub_page_params, can_add_sub_page),
|
||||
)
|
||||
|
||||
for title, params, has_perm in add_page_menu_modal_items:
|
||||
params.update(language=self.toolbar.request_language)
|
||||
add_page_menu.add_modal_item(
|
||||
title,
|
||||
url=add_url_parameters(add_page_url, params),
|
||||
disabled=not has_perm,
|
||||
)
|
||||
|
||||
add_page_menu.add_modal_item(
|
||||
_('Duplicate this Page'),
|
||||
url=add_url_parameters(duplicate_page_url, {'language': self.toolbar.request_language}),
|
||||
disabled=not can_add_sibling_page,
|
||||
)
|
||||
|
||||
# first break
|
||||
current_page_menu.add_break(PAGE_MENU_FIRST_BREAK)
|
||||
|
||||
# page edit
|
||||
page_edit_url = '?%s' % get_cms_setting('CMS_TOOLBAR_URL__EDIT_ON')
|
||||
current_page_menu.add_link_item(_('Edit this Page'), disabled=edit_mode, url=page_edit_url)
|
||||
|
||||
# page settings
|
||||
page_settings_url = add_url_parameters(page_settings_url, language=self.toolbar.request_language)
|
||||
settings_disabled = not edit_mode or not can_change
|
||||
current_page_menu.add_modal_item(_('Page settings'), url=page_settings_url, disabled=settings_disabled,
|
||||
on_close=refresh)
|
||||
|
||||
# advanced settings
|
||||
advanced_url = add_url_parameters(advanced_url, language=self.toolbar.request_language)
|
||||
can_change_advanced = self.page.has_advanced_settings_permission(self.request.user)
|
||||
advanced_disabled = not edit_mode or not can_change_advanced
|
||||
current_page_menu.add_modal_item(_('Advanced settings'), url=advanced_url, disabled=advanced_disabled)
|
||||
|
||||
# templates menu
|
||||
if edit_mode:
|
||||
if self.page.is_page_type:
|
||||
action = admin_reverse('cms_pagetype_change_template', args=(self.page.pk,))
|
||||
else:
|
||||
action = admin_reverse('cms_page_change_template', args=(self.page.pk,))
|
||||
|
||||
if can_change_advanced:
|
||||
templates_menu = current_page_menu.get_or_create_menu(
|
||||
'templates',
|
||||
_('Templates'),
|
||||
disabled=not can_change,
|
||||
)
|
||||
|
||||
for path, name in get_cms_setting('TEMPLATES'):
|
||||
active = self.page.template == path
|
||||
if path == TEMPLATE_INHERITANCE_MAGIC:
|
||||
templates_menu.add_break(TEMPLATE_MENU_BREAK)
|
||||
templates_menu.add_ajax_item(name, action=action, data={'template': path}, active=active,
|
||||
on_success=refresh)
|
||||
|
||||
# page type
|
||||
if not self.page.is_page_type:
|
||||
page_type_url = admin_reverse('cms_pagetype_add')
|
||||
page_type_url = add_url_parameters(page_type_url, source=self.page.pk, language=self.toolbar.request_language)
|
||||
page_type_disabled = not edit_mode or not can_add_root_page
|
||||
current_page_menu.add_modal_item(_('Save as Page Type'), page_type_url, disabled=page_type_disabled)
|
||||
|
||||
# second break
|
||||
current_page_menu.add_break(PAGE_MENU_SECOND_BREAK)
|
||||
|
||||
# permissions
|
||||
if self.permissions_activated:
|
||||
permissions_url = admin_reverse('cms_page_permissions', args=(self.page.pk,))
|
||||
permission_disabled = not edit_mode
|
||||
|
||||
if not permission_disabled:
|
||||
permission_disabled = not page_permissions.user_can_change_page_permissions(
|
||||
user=self.request.user,
|
||||
page=self.page,
|
||||
)
|
||||
current_page_menu.add_modal_item(_('Permissions'), url=permissions_url, disabled=permission_disabled)
|
||||
|
||||
if not self.page.is_page_type:
|
||||
# dates settings
|
||||
dates_url = admin_reverse('cms_page_dates', args=(self.page.pk,))
|
||||
current_page_menu.add_modal_item(
|
||||
_('Publishing dates'),
|
||||
url=dates_url,
|
||||
disabled=(not edit_mode or not can_change),
|
||||
)
|
||||
|
||||
# third break
|
||||
current_page_menu.add_break(PAGE_MENU_THIRD_BREAK)
|
||||
|
||||
# navigation toggle
|
||||
nav_title = _('Hide in navigation') if self.page.in_navigation else _('Display in navigation')
|
||||
nav_action = admin_reverse('cms_page_change_innavigation', args=(self.page.pk,))
|
||||
current_page_menu.add_ajax_item(
|
||||
nav_title,
|
||||
action=nav_action,
|
||||
disabled=(not edit_mode or not can_change),
|
||||
on_success=refresh,
|
||||
)
|
||||
|
||||
# publisher
|
||||
if self.title and not self.page.is_page_type:
|
||||
if self.title.published:
|
||||
publish_title = _('Unpublish page')
|
||||
publish_url = admin_reverse('cms_page_unpublish', args=(self.page.pk, self.current_lang))
|
||||
else:
|
||||
publish_title = _('Publish page')
|
||||
publish_url = admin_reverse('cms_page_publish_page', args=(self.page.pk, self.current_lang))
|
||||
|
||||
user_can_publish = user_can_publish_page(self.request.user, page=self.page)
|
||||
current_page_menu.add_ajax_item(
|
||||
publish_title,
|
||||
action=publish_url,
|
||||
disabled=not edit_mode or not user_can_publish,
|
||||
on_success=refresh,
|
||||
)
|
||||
|
||||
if self.current_lang and not self.page.is_page_type:
|
||||
# revert to live
|
||||
current_page_menu.add_break(PAGE_MENU_FOURTH_BREAK)
|
||||
revert_action = admin_reverse('cms_page_revert_to_live', args=(self.page.pk, self.current_lang))
|
||||
revert_question = _('Are you sure you want to revert to live?')
|
||||
# Only show this action if the page has pending changes and a public version
|
||||
is_enabled = (
|
||||
edit_mode
|
||||
and can_change
|
||||
and self.page.is_dirty(self.current_lang)
|
||||
and self.page.publisher_public
|
||||
)
|
||||
current_page_menu.add_ajax_item(
|
||||
_('Revert to live'),
|
||||
action=revert_action,
|
||||
question=revert_question,
|
||||
disabled=not is_enabled,
|
||||
on_success=refresh,
|
||||
extra_classes=('cms-toolbar-revert',),
|
||||
)
|
||||
|
||||
# last break
|
||||
current_page_menu.add_break(PAGE_MENU_LAST_BREAK)
|
||||
|
||||
# delete
|
||||
if self.page.is_page_type:
|
||||
delete_url = admin_reverse('cms_pagetype_delete', args=(self.page.pk,))
|
||||
else:
|
||||
delete_url = admin_reverse('cms_page_delete', args=(self.page.pk,))
|
||||
delete_disabled = not edit_mode or not user_can_delete_page(self.request.user, page=self.page)
|
||||
on_delete_redirect_url = self.get_on_delete_redirect_url()
|
||||
current_page_menu.add_modal_item(_('Delete page'), url=delete_url, on_close=on_delete_redirect_url,
|
||||
disabled=delete_disabled)
|
||||
@@ -1,57 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from cms.models import Page
|
||||
from cms.utils.page_permissions import user_can_add_page, user_can_add_subpage
|
||||
|
||||
from .wizards.wizard_pool import wizard_pool
|
||||
from .wizards.wizard_base import Wizard
|
||||
|
||||
from .forms.wizards import CreateCMSPageForm, CreateCMSSubPageForm
|
||||
|
||||
|
||||
class CMSPageWizard(Wizard):
|
||||
|
||||
def user_has_add_permission(self, user, page=None, **kwargs):
|
||||
if page:
|
||||
parent_page = page.get_parent_page()
|
||||
else:
|
||||
parent_page = None
|
||||
|
||||
if page and page.get_parent_page():
|
||||
# User is adding a page which will be a right
|
||||
# sibling to the current page.
|
||||
has_perm = user_can_add_subpage(user, target=parent_page)
|
||||
else:
|
||||
has_perm = user_can_add_page(user)
|
||||
return has_perm
|
||||
|
||||
|
||||
class CMSSubPageWizard(Wizard):
|
||||
|
||||
def user_has_add_permission(self, user, page=None, **kwargs):
|
||||
if not page or page.application_urls:
|
||||
# We can't really add a sub-page to a non-existent page. Or to an
|
||||
# app-hooked page.
|
||||
return False
|
||||
return user_can_add_subpage(user, target=page)
|
||||
|
||||
|
||||
cms_page_wizard = CMSPageWizard(
|
||||
title=_(u"New page"),
|
||||
weight=100,
|
||||
form=CreateCMSPageForm,
|
||||
model=Page,
|
||||
description=_(u"Create a new page next to the current page.")
|
||||
)
|
||||
|
||||
cms_subpage_wizard = CMSSubPageWizard(
|
||||
title=_(u"New sub page"),
|
||||
weight=110,
|
||||
form=CreateCMSSubPageForm,
|
||||
model=Page,
|
||||
description=_(u"Create a page below the current page.")
|
||||
)
|
||||
|
||||
wizard_pool.register(cms_page_wizard)
|
||||
wizard_pool.register(cms_subpage_wizard)
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
TEMPLATE_INHERITANCE_MAGIC = 'INHERIT'
|
||||
REFRESH_PAGE = 'REFRESH_PAGE'
|
||||
FOLLOW_REDIRECT = 'FOLLOW_REDIRECT'
|
||||
URL_CHANGE = 'URL_CHANGE'
|
||||
RIGHT = object() # this is a trick so "foo is RIGHT" will only ever work for this, same goes for LEFT.
|
||||
LEFT = object()
|
||||
|
||||
PUBLISHER_STATE_DEFAULT = 0
|
||||
PUBLISHER_STATE_DIRTY = 1
|
||||
# Page was marked published, but some of page parents are not.
|
||||
PUBLISHER_STATE_PENDING = 4
|
||||
|
||||
PAGE_TYPES_ID = "page_types"
|
||||
PAGE_TREE_POSITIONS = ('last-child', 'first-child', 'left', 'right')
|
||||
|
||||
VISIBILITY_ALL = None
|
||||
VISIBILITY_USERS = 1
|
||||
VISIBILITY_ANONYMOUS = 2
|
||||
|
||||
X_FRAME_OPTIONS_INHERIT = 0
|
||||
X_FRAME_OPTIONS_DENY = 1
|
||||
X_FRAME_OPTIONS_SAMEORIGIN = 2
|
||||
X_FRAME_OPTIONS_ALLOW = 3
|
||||
|
||||
PAGE_USERNAME_MAX_LENGTH = 255
|
||||
|
||||
SLUG_REGEXP = '[0-9A-Za-z-_.//]+'
|
||||
|
||||
EXPIRE_NOW = 0
|
||||
# HTTP Specification says max caching should only be up to one year.
|
||||
MAX_EXPIRATION_TTL = 365 * 24 * 3600
|
||||
|
||||
PLUGIN_TOOLBAR_JS = "CMS._plugins.push([\"cms-plugin-%(pk)s\", %(config)s]);\n"
|
||||
|
||||
PLACEHOLDER_TOOLBAR_JS = "CMS._plugins.push([\"cms-placeholder-%(pk)s\", %(config)s]);"
|
||||
|
||||
# In the permissions system we use user levels to determine
|
||||
# the depth in which the user has permissions.
|
||||
# This constant represents a user that can see pages at all depths.
|
||||
ROOT_USER_LEVEL = -1
|
||||
|
||||
GRANT_ALL_PERMISSIONS = 'All'
|
||||
|
||||
PUBLISH_COMMENT = "Publish"
|
||||
|
||||
SCRIPT_USERNAME = 'script'
|
||||
@@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.utils import lru_cache
|
||||
from django.utils.functional import lazy
|
||||
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.page import get_page_template_from_request
|
||||
|
||||
|
||||
def cms_settings(request):
|
||||
"""
|
||||
Adds cms-related variables to the context.
|
||||
"""
|
||||
from menus.menu_pool import MenuRenderer
|
||||
|
||||
@lru_cache.lru_cache(maxsize=None)
|
||||
def _get_menu_renderer():
|
||||
# We use lru_cache to avoid getting the manager
|
||||
# every time this function is called.
|
||||
from menus.menu_pool import menu_pool
|
||||
return menu_pool.get_renderer(request)
|
||||
|
||||
# Now use lazy() to avoid getting the menu renderer
|
||||
# up until the point is needed.
|
||||
# lazy() does not memoize results, is why lru_cache is needed.
|
||||
_get_menu_renderer = lazy(_get_menu_renderer, MenuRenderer)
|
||||
|
||||
return {
|
||||
'cms_menu_renderer': _get_menu_renderer(),
|
||||
'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'),
|
||||
'CMS_TEMPLATE': lambda: get_page_template_from_request(request),
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
class PluginAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PluginNotRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PluginLimitReached(Exception):
|
||||
"""
|
||||
Gets triggered when a placeholder has reached it's plugin limit.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AppAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ToolbarAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ToolbarNotRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotImplemented(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SubClassNeededError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingFormError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoHomeFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PermissionsException(Exception):
|
||||
"""Base permission exception
|
||||
"""
|
||||
|
||||
|
||||
class NoPermissionsException(PermissionsException):
|
||||
"""Can be fired when some violate action is performed on permission system.
|
||||
"""
|
||||
|
||||
|
||||
class PublicIsUnmodifiable(Exception):
|
||||
"""A method was invoked on the public copy, but is only valid for the
|
||||
draft version"""
|
||||
pass
|
||||
|
||||
|
||||
class PublicVersionNeeded(Exception):
|
||||
"""A Public version of this page is needed"""
|
||||
pass
|
||||
|
||||
|
||||
class Deprecated(Exception): pass
|
||||
|
||||
|
||||
class DuplicatePlaceholderWarning(Warning): pass
|
||||
|
||||
|
||||
class DontUsePageAttributeWarning(Warning): pass
|
||||
|
||||
|
||||
class CMSDeprecationWarning(Warning): pass
|
||||
|
||||
|
||||
class LanguageError(Exception): pass
|
||||
|
||||
|
||||
class PluginConsistencyError(Exception): pass
|
||||
|
||||
|
||||
class PlaceholderNotFound(Exception): pass
|
||||
@@ -1,5 +0,0 @@
|
||||
from .models import PageExtension # nopyflakes
|
||||
from .models import TitleExtension # nopyflakes
|
||||
from .extension_pool import extension_pool # nopyflakes
|
||||
from .admin import PageExtensionAdmin # nopyflakes
|
||||
from .admin import TitleExtensionAdmin # nopyflakes
|
||||
@@ -1,109 +0,0 @@
|
||||
from cms.models import Page, Title
|
||||
from cms.utils.page_permissions import user_can_change_page
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import csrf_protect_m
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ExtensionAdmin(admin.ModelAdmin):
|
||||
change_form_template = "admin/cms/extensions/change_form.html"
|
||||
add_form_template = "admin/cms/extensions/change_form.html"
|
||||
|
||||
|
||||
class PageExtensionAdmin(ExtensionAdmin):
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change and 'extended_object' in request.GET:
|
||||
obj.extended_object = Page.objects.get(pk=request.GET['extended_object'])
|
||||
page = Page.objects.get(pk=request.GET['extended_object'])
|
||||
else:
|
||||
page = obj.extended_object
|
||||
if not user_can_change_page(request.user, page):
|
||||
raise PermissionDenied()
|
||||
super(PageExtensionAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
def delete_model(self, request, obj):
|
||||
if not obj.extended_object.has_change_permission(request.user):
|
||||
raise PermissionDenied()
|
||||
obj.delete()
|
||||
|
||||
def get_model_perms(self, request):
|
||||
"""
|
||||
Return empty perms dict thus hiding the model from admin index.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(PageExtensionAdmin, self).get_queryset(request).filter(extended_object__publisher_is_draft=True)
|
||||
|
||||
@csrf_protect_m
|
||||
def add_view(self, request, form_url='', extra_context=None):
|
||||
"""
|
||||
Check if the page already has an extension object. If so, redirect to edit view instead.
|
||||
"""
|
||||
extended_object_id = request.GET.get('extended_object', False)
|
||||
if extended_object_id:
|
||||
try:
|
||||
page = Page.objects.get(pk=extended_object_id)
|
||||
extension = self.model.objects.get(extended_object=page)
|
||||
opts = self.model._meta
|
||||
change_url = reverse('admin:%s_%s_change' %
|
||||
(opts.app_label, opts.model_name),
|
||||
args=(extension.pk,),
|
||||
current_app=self.admin_site.name)
|
||||
return HttpResponseRedirect(change_url)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
return super(ExtensionAdmin, self).add_view(request, form_url, extra_context)
|
||||
|
||||
|
||||
class TitleExtensionAdmin(ExtensionAdmin):
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change and 'extended_object' in request.GET:
|
||||
obj.extended_object = Title.objects.get(pk=request.GET['extended_object'])
|
||||
title = Title.objects.get(pk=request.GET['extended_object'])
|
||||
else:
|
||||
title = obj.extended_object
|
||||
if not user_can_change_page(request.user, page=title.page):
|
||||
raise PermissionDenied()
|
||||
super(TitleExtensionAdmin, self).save_model(request, obj, form, change)
|
||||
|
||||
def delete_model(self, request, obj):
|
||||
page = obj.extended_object.page
|
||||
|
||||
if not user_can_change_page(request.user, page):
|
||||
raise PermissionDenied()
|
||||
obj.delete()
|
||||
|
||||
def get_model_perms(self, request):
|
||||
"""
|
||||
Return empty perms dict thus hiding the model from admin index.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super(TitleExtensionAdmin, self).get_queryset(request).filter(extended_object__page__publisher_is_draft=True)
|
||||
|
||||
@csrf_protect_m
|
||||
def add_view(self, request, form_url='', extra_context=None):
|
||||
"""
|
||||
Check if the page already has an extension object. If so, redirect to edit view instead.
|
||||
"""
|
||||
extended_object_id = request.GET.get('extended_object', False)
|
||||
if extended_object_id:
|
||||
try:
|
||||
title = Title.objects.get(pk=extended_object_id)
|
||||
extension = self.model.objects.get(extended_object=title)
|
||||
opts = self.model._meta
|
||||
change_url = reverse('admin:%s_%s_change' %
|
||||
(opts.app_label, opts.model_name),
|
||||
args=(extension.pk,),
|
||||
current_app=self.admin_site.name)
|
||||
return HttpResponseRedirect(change_url)
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
return super(ExtensionAdmin, self).add_view(request, form_url, extra_context)
|
||||
@@ -1,148 +0,0 @@
|
||||
from cms.exceptions import SubClassNeededError
|
||||
|
||||
from .models import PageExtension, TitleExtension
|
||||
|
||||
|
||||
class ExtensionPool(object):
|
||||
def __init__(self):
|
||||
self.page_extensions = set()
|
||||
self.title_extensions = set()
|
||||
self.signaling_activated = False
|
||||
|
||||
def register(self, extension):
|
||||
"""
|
||||
Registers the given extension.
|
||||
|
||||
Example::
|
||||
|
||||
class MyExtension(PageExtension):
|
||||
pass
|
||||
|
||||
extension_pool.register(MyExtension)
|
||||
|
||||
or as decorator::
|
||||
|
||||
@extension_pool.register
|
||||
class MyExtension(PageExtension):
|
||||
pass
|
||||
|
||||
"""
|
||||
|
||||
if issubclass(extension, PageExtension):
|
||||
self.page_extensions.add(extension)
|
||||
elif issubclass(extension, TitleExtension):
|
||||
self.title_extensions.add(extension)
|
||||
else:
|
||||
raise SubClassNeededError(
|
||||
'Extension has to subclass either %r or %r. %r does not!' % (PageExtension, TitleExtension, extension)
|
||||
)
|
||||
self._activate_signaling()
|
||||
return extension
|
||||
|
||||
def unregister(self, extension):
|
||||
"""
|
||||
Unregisters the given extension. No error is thrown if given extension isn't an extension or wasn't
|
||||
registered yet.
|
||||
"""
|
||||
|
||||
try:
|
||||
if issubclass(extension, PageExtension):
|
||||
self.page_extensions.remove(extension)
|
||||
elif issubclass(extension, TitleExtension):
|
||||
self.title_extensions.remove(extension)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _activate_signaling(self):
|
||||
"""
|
||||
Activates the post_publish signal receiver if not already done.
|
||||
"""
|
||||
|
||||
if not self.signaling_activated:
|
||||
from cms.signals import post_publish
|
||||
post_publish.connect(self._receiver)
|
||||
self.signaling_activated = True
|
||||
|
||||
def _receiver(self, sender, **kwargs):
|
||||
"""
|
||||
Receiver for the post_publish signal. Gets the published page from kwargs.
|
||||
"""
|
||||
|
||||
# instance from kwargs is the draft page
|
||||
draft_page = kwargs.get('instance')
|
||||
language = kwargs.get('language')
|
||||
# get the new public page from the draft page
|
||||
public_page = draft_page.publisher_public
|
||||
|
||||
if self.page_extensions:
|
||||
self._copy_page_extensions(draft_page, public_page, language, clone=False)
|
||||
self._remove_orphaned_page_extensions()
|
||||
if self.title_extensions:
|
||||
self._copy_title_extensions(draft_page, None, language, clone=False)
|
||||
self._remove_orphaned_title_extensions()
|
||||
|
||||
def _copy_page_extensions(self, source_page, target_page, language, clone=False):
|
||||
for extension in self.page_extensions:
|
||||
for instance in extension.objects.filter(extended_object=source_page):
|
||||
if clone:
|
||||
instance.copy(target_page, language)
|
||||
else:
|
||||
instance.copy_to_public(target_page, language)
|
||||
|
||||
def _copy_title_extensions(self, source_page, target_page, language, clone=False):
|
||||
source_title = source_page.title_set.get(language=language)
|
||||
if target_page:
|
||||
target_title = target_page.title_set.get(language=language)
|
||||
else:
|
||||
target_title = source_title.publisher_public
|
||||
for extension in self.title_extensions:
|
||||
for instance in extension.objects.filter(extended_object=source_title):
|
||||
if clone:
|
||||
instance.copy(target_title, language)
|
||||
else:
|
||||
instance.copy_to_public(target_title, language)
|
||||
|
||||
def copy_extensions(self, source_page, target_page, languages=None):
|
||||
if not languages:
|
||||
languages = target_page.get_languages()
|
||||
if self.page_extensions:
|
||||
self._copy_page_extensions(source_page, target_page, None, clone=True)
|
||||
self._remove_orphaned_page_extensions()
|
||||
for language in languages:
|
||||
if self.title_extensions:
|
||||
self._copy_title_extensions(source_page, target_page, language, clone=True)
|
||||
self._remove_orphaned_title_extensions()
|
||||
|
||||
def _remove_orphaned_page_extensions(self):
|
||||
for extension in self.page_extensions:
|
||||
extension.objects.filter(
|
||||
extended_object__publisher_is_draft=False,
|
||||
draft_extension=None
|
||||
).delete()
|
||||
|
||||
def _remove_orphaned_title_extensions(self):
|
||||
for extension in self.title_extensions:
|
||||
extension.objects.filter(
|
||||
extended_object__page__publisher_is_draft=False,
|
||||
draft_extension=None
|
||||
).delete()
|
||||
|
||||
def get_page_extensions(self, page=None):
|
||||
extensions = []
|
||||
for extension in self.page_extensions:
|
||||
if page:
|
||||
extensions.extend(list(extension.objects.filter(extended_object=page)))
|
||||
else:
|
||||
extensions.extend(list(extension.objects.all()))
|
||||
return extensions
|
||||
|
||||
def get_title_extensions(self, title=None):
|
||||
extensions = []
|
||||
for extension in self.title_extensions:
|
||||
if title:
|
||||
extensions.extend(list(extension.objects.filter(extended_object=title)))
|
||||
else:
|
||||
extensions.extend(list(extension.objects.all()))
|
||||
return extensions
|
||||
|
||||
extension_pool = ExtensionPool()
|
||||
@@ -1,152 +0,0 @@
|
||||
from django.db.models import ManyToManyField
|
||||
|
||||
from cms.constants import PUBLISHER_STATE_DIRTY
|
||||
from django.db import models
|
||||
|
||||
from cms.models import Page, Title
|
||||
|
||||
|
||||
class BaseExtension(models.Model):
|
||||
public_extension = models.OneToOneField(
|
||||
'self',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
editable=False,
|
||||
related_name='draft_extension',
|
||||
)
|
||||
extended_object = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_page(self): # pragma: no cover
|
||||
raise NotImplementedError('Function must be overwritten in subclasses and return the extended page object.')
|
||||
|
||||
def copy_relations(self, oldinstance, language):
|
||||
"""
|
||||
Copy relations like many to many or foreign key relations to the public version.
|
||||
Similar to the same named cms plugin function.
|
||||
|
||||
:param oldinstance: the draft version of the extension
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _get_related_objects(cls):
|
||||
fields = cls._meta._get_fields(
|
||||
forward=False, reverse=True,
|
||||
include_parents=True,
|
||||
include_hidden=False,
|
||||
)
|
||||
return list(obj for obj in fields if not isinstance(obj.field, ManyToManyField))
|
||||
|
||||
def copy(self, target, language):
|
||||
"""
|
||||
This method copies this extension to an unrelated-target. If you intend
|
||||
to "publish" this extension to the publisher counterpart of target, then
|
||||
use copy_to_publish() instead.
|
||||
"""
|
||||
clone = self.__class__.objects.get(pk=self.pk) # get a copy of this instance
|
||||
clone.pk = None
|
||||
clone.public_extension = None
|
||||
clone.extended_object = target # set the new public object
|
||||
|
||||
# Nullify all concrete parent primary keys. See issue #5494
|
||||
for parent, field in clone._meta.parents.items():
|
||||
if field:
|
||||
setattr(clone, parent._meta.pk.attname, None)
|
||||
|
||||
clone.save(mark_page=False)
|
||||
|
||||
# If the target we're copying already has a publisher counterpart, then
|
||||
# connect the dots.
|
||||
target_prime = getattr(target, 'publisher_public')
|
||||
if target_prime:
|
||||
related_name = self.__class__.__name__.lower()
|
||||
clone_prime = getattr(target_prime, related_name)
|
||||
if clone_prime:
|
||||
clone.public_extension = clone_prime
|
||||
else:
|
||||
clone.public_extension = None
|
||||
|
||||
clone.copy_relations(self, language)
|
||||
clone.save(force_update=True, mark_page=False)
|
||||
return clone
|
||||
|
||||
def copy_to_public(self, public_object, language):
|
||||
"""
|
||||
This method is used to "publish" this extension as part of the a larger
|
||||
operation on the target. If you intend to copy this extension to an
|
||||
unrelated object, use copy() instead.
|
||||
"""
|
||||
this = self.__class__.objects.get(pk=self.pk) # get a copy of this instance
|
||||
public_extension = self.public_extension # get the public version of this instance if any
|
||||
|
||||
this.extended_object = public_object # set the new public object
|
||||
|
||||
if public_extension:
|
||||
this.pk = public_extension.pk # overwrite current public extension
|
||||
this.public_extension = None # remove public extension or it will point to itself and raise duplicate entry
|
||||
|
||||
# Set public_extension concrete parents PKs. See issue #5494
|
||||
for parent, field in this._meta.parents.items():
|
||||
if field:
|
||||
setattr(this, parent._meta.pk.attname, getattr(public_extension, parent._meta.pk.attname))
|
||||
else:
|
||||
this.pk = None # create new public extension
|
||||
|
||||
# Nullify all concrete parent primary keys. See issue #5494
|
||||
for parent, field in this._meta.parents.items():
|
||||
if field:
|
||||
setattr(this, parent._meta.pk.attname, None)
|
||||
|
||||
this.save(mark_page=False)
|
||||
self.public_extension = this
|
||||
self.save(mark_page=False)
|
||||
|
||||
this.copy_relations(self, language)
|
||||
this.save(force_update=True, mark_page=False)
|
||||
|
||||
return this
|
||||
|
||||
|
||||
class PageExtension(BaseExtension):
|
||||
extended_object = models.OneToOneField(Page, on_delete=models.CASCADE, editable=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_page(self):
|
||||
return self.extended_object
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if kwargs.pop('mark_page', True):
|
||||
self.get_page().title_set.update(publisher_state=PUBLISHER_STATE_DIRTY) # mark page dirty
|
||||
return super(BaseExtension, self).save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if kwargs.pop('mark_page', True):
|
||||
self.get_page().title_set.update(publisher_state=PUBLISHER_STATE_DIRTY) # mark page dirty
|
||||
return super(BaseExtension, self).delete(*args, **kwargs)
|
||||
|
||||
|
||||
class TitleExtension(BaseExtension):
|
||||
extended_object = models.OneToOneField(Title, on_delete=models.CASCADE, editable=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get_page(self):
|
||||
return self.extended_object.page
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if kwargs.pop('mark_page', True):
|
||||
Title.objects.filter(pk=self.extended_object.pk).update(
|
||||
publisher_state=PUBLISHER_STATE_DIRTY) # mark title dirty
|
||||
return super(BaseExtension, self).save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if kwargs.pop('mark_page', True):
|
||||
Title.objects.filter(pk=self.extended_object.pk).update(
|
||||
publisher_state=PUBLISHER_STATE_DIRTY) # mark title dirty
|
||||
return super(BaseExtension, self).delete(*args, **kwargs)
|
||||
@@ -1,110 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from cms.utils.urlutils import admin_reverse
|
||||
from cms.api import get_page_draft
|
||||
from cms.toolbar_base import CMSToolbar
|
||||
from cms.utils import get_language_list
|
||||
from cms.utils.page_permissions import user_can_change_page
|
||||
|
||||
from django.urls import NoReverseMatch
|
||||
|
||||
|
||||
class ExtensionToolbar(CMSToolbar):
|
||||
model = None
|
||||
page = None
|
||||
|
||||
def _setup_extension_toolbar(self):
|
||||
"""
|
||||
Does all the sanity check for the current environment:
|
||||
|
||||
* that a page exists
|
||||
* permissions check on the current page
|
||||
|
||||
It returns the page menu or None if the above conditions are not met
|
||||
"""
|
||||
page = self._get_page()
|
||||
|
||||
if page and user_can_change_page(self.request.user, page=page):
|
||||
return self.toolbar.get_or_create_menu('page')
|
||||
return
|
||||
|
||||
def _get_page(self):
|
||||
"""
|
||||
A utility method that caches the current page and make sure to use the draft version of the page.
|
||||
"""
|
||||
# always use draft if we have a page
|
||||
if not self.page:
|
||||
self.page = get_page_draft(self.request.current_page)
|
||||
return self.page
|
||||
|
||||
def get_page_extension_admin(self):
|
||||
"""
|
||||
Get the admin url for the page extension menu item, depending on whether a PageExtension instance exists
|
||||
for the current page or not.
|
||||
|
||||
Return a tuple of the current extension and the url; the extension is None if no instance exists,
|
||||
the url is None is no admin is registered for the extension.
|
||||
"""
|
||||
page = self._get_page()
|
||||
# Page extension
|
||||
try:
|
||||
page_extension = self.model.objects.get(extended_object_id=page.pk)
|
||||
except self.model.DoesNotExist:
|
||||
page_extension = None
|
||||
try:
|
||||
model_name = self.model.__name__.lower()
|
||||
if page_extension:
|
||||
admin_url = admin_reverse(
|
||||
'%s_%s_change' % (self.model._meta.app_label, model_name),
|
||||
args=(page_extension.pk,))
|
||||
else:
|
||||
admin_url = "%s?extended_object=%s" % (
|
||||
admin_reverse('%s_%s_add' % (self.model._meta.app_label, model_name)),
|
||||
self.page.pk)
|
||||
except NoReverseMatch: # pragma: no cover
|
||||
admin_url = None
|
||||
return page_extension, admin_url
|
||||
|
||||
def get_title_extension_admin(self, language=None):
|
||||
"""
|
||||
Get the admin urls for the title extensions menu items, depending on whether a TitleExtension instance exists
|
||||
for each Title in the current page.
|
||||
A single language can be passed to only work on a single title.
|
||||
|
||||
Return a list of tuples of the title extension and the url; the extension is None if no instance exists,
|
||||
the url is None is no admin is registered for the extension.
|
||||
"""
|
||||
page = self._get_page()
|
||||
urls = []
|
||||
if language:
|
||||
titles = page.get_title_obj(language),
|
||||
else:
|
||||
titles = page.title_set.filter(language__in=get_language_list(page.node.site_id))
|
||||
# Titles
|
||||
for title in titles:
|
||||
try:
|
||||
title_extension = self.model.objects.get(extended_object_id=title.pk)
|
||||
except self.model.DoesNotExist:
|
||||
title_extension = None
|
||||
try:
|
||||
model_name = self.model.__name__.lower()
|
||||
if title_extension:
|
||||
admin_url = admin_reverse(
|
||||
'%s_%s_change' % (self.model._meta.app_label, model_name),
|
||||
args=(title_extension.pk,))
|
||||
else:
|
||||
admin_url = "%s?extended_object=%s" % (
|
||||
admin_reverse('%s_%s_add' % (self.model._meta.app_label, model_name)),
|
||||
title.pk)
|
||||
except NoReverseMatch: # pragma: no cover
|
||||
admin_url = None
|
||||
if admin_url:
|
||||
urls.append((title_extension, admin_url))
|
||||
return urls
|
||||
|
||||
def _get_sub_menu(self, current_menu, key, label, position=None):
|
||||
"""
|
||||
Utility function to get a submenu of the current menu
|
||||
"""
|
||||
extension_menu = current_menu.get_or_create_menu(
|
||||
key, label, position=position)
|
||||
return extension_menu
|
||||
@@ -1,98 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
||||
from django.forms.fields import EMPTY_VALUES
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from cms.forms.utils import get_site_choices, get_page_choices
|
||||
from cms.forms.validators import validate_url
|
||||
from cms.forms.widgets import PageSelectWidget, PageSmartLinkWidget
|
||||
from cms.models.pagemodel import Page
|
||||
|
||||
|
||||
class SuperLazyIterator(object):
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.func())
|
||||
|
||||
|
||||
class LazyChoiceField(forms.ChoiceField):
|
||||
def _set_choices(self, value):
|
||||
# we overwrite this function so no list(value) is called
|
||||
self._choices = self.widget.choices = value
|
||||
|
||||
choices = property(forms.ChoiceField._get_choices, _set_choices)
|
||||
|
||||
|
||||
class PageSelectFormField(forms.MultiValueField):
|
||||
widget = PageSelectWidget
|
||||
default_error_messages = {
|
||||
'invalid_site': _(u'Select a valid site'),
|
||||
'invalid_page': _(u'Select a valid page'),
|
||||
}
|
||||
|
||||
def __init__(self, queryset=None, empty_label=u"---------", cache_choices=False,
|
||||
required=True, widget=None, to_field_name=None, limit_choices_to=None,
|
||||
*args, **kwargs):
|
||||
errors = self.default_error_messages.copy()
|
||||
if 'error_messages' in kwargs:
|
||||
errors.update(kwargs['error_messages'])
|
||||
site_choices = SuperLazyIterator(get_site_choices)
|
||||
page_choices = SuperLazyIterator(get_page_choices)
|
||||
self.limit_choices_to = limit_choices_to
|
||||
kwargs['required'] = required
|
||||
fields = (
|
||||
LazyChoiceField(choices=site_choices, required=False, error_messages={'invalid': errors['invalid_site']}),
|
||||
LazyChoiceField(choices=page_choices, required=False, error_messages={'invalid': errors['invalid_page']}),
|
||||
)
|
||||
super(PageSelectFormField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
if data_list:
|
||||
page_id = data_list[1]
|
||||
|
||||
if page_id in EMPTY_VALUES:
|
||||
if not self.required:
|
||||
return None
|
||||
raise forms.ValidationError(self.error_messages['invalid_page'])
|
||||
return Page.objects.get(pk=page_id)
|
||||
return None
|
||||
|
||||
def has_changed(self, initial, data):
|
||||
is_empty = data and (len(data) >= 2 and data[1] in [None, ''])
|
||||
|
||||
if isinstance(self.widget, RelatedFieldWidgetWrapper):
|
||||
self.widget.decompress = self.widget.widget.decompress
|
||||
|
||||
if is_empty and initial is None:
|
||||
# when empty data will have [u'1', u'', u''] as value
|
||||
# this will cause django to always return True because of the '1'
|
||||
# so we simply follow django's default behavior when initial is None and data is "empty"
|
||||
data = ['' for x in range(0, len(data))]
|
||||
return super(PageSelectFormField, self).has_changed(initial, data)
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
return self.has_changed(initial, data)
|
||||
|
||||
|
||||
class PageSmartLinkField(forms.CharField):
|
||||
widget = PageSmartLinkWidget
|
||||
default_validators = [validate_url]
|
||||
|
||||
def __init__(self, max_length=None, min_length=None, placeholder_text=None,
|
||||
ajax_view=None, *args, **kwargs):
|
||||
self.placeholder_text = placeholder_text
|
||||
widget = self.widget(ajax_view=ajax_view)
|
||||
super(PageSmartLinkField, self).__init__(max_length=max_length, min_length=min_length,
|
||||
widget=widget, *args, **kwargs)
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
attrs = super(PageSmartLinkField, self).widget_attrs(widget)
|
||||
attrs.update({'placeholder_text': self.placeholder_text})
|
||||
return attrs
|
||||
|
||||
def clean(self, value):
|
||||
value = self.to_python(value).strip()
|
||||
return super(PageSmartLinkField, self).clean(value)
|
||||
@@ -1,13 +0,0 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
|
||||
|
||||
class CMSToolbarLoginForm(AuthenticationForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CMSToolbarLoginForm, self).__init__(*args, **kwargs)
|
||||
kwargs['prefix'] = kwargs.get('prefix', 'cms')
|
||||
self.fields['username'].widget = forms.TextInput(
|
||||
attrs = { 'required': 'required' })
|
||||
self.fields['password'].widget = forms.PasswordInput(
|
||||
attrs = { 'required': 'required' })
|
||||
@@ -1,104 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Prefetch
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from cms.cache.choices import (
|
||||
clean_site_choices_cache, clean_page_choices_cache,
|
||||
_site_cache_key, _page_cache_key)
|
||||
from cms.models import Page, Title
|
||||
from cms.utils import i18n
|
||||
|
||||
|
||||
def get_sites():
|
||||
sites = (
|
||||
Site
|
||||
.objects
|
||||
.filter(djangocms_nodes__isnull=False)
|
||||
.order_by('name')
|
||||
.distinct()
|
||||
)
|
||||
return sites
|
||||
|
||||
|
||||
def get_page_choices_for_site(site, language):
|
||||
fallbacks = i18n.get_fallback_languages(language, site_id=site.pk)
|
||||
languages = [language] + fallbacks
|
||||
translation_lookup = Prefetch(
|
||||
'title_set',
|
||||
to_attr='filtered_translations',
|
||||
queryset=Title.objects.filter(language__in=languages).only('pk', 'page', 'language', 'title')
|
||||
)
|
||||
pages = (
|
||||
Page
|
||||
.objects
|
||||
.drafts()
|
||||
.on_site(site)
|
||||
.select_related('node')
|
||||
.prefetch_related(translation_lookup)
|
||||
.order_by('node__path')
|
||||
.only('pk', 'node')
|
||||
)
|
||||
|
||||
for page in pages:
|
||||
translations = page.filtered_translations
|
||||
titles_by_language = {trans.language: trans.title for trans in translations}
|
||||
|
||||
for language in languages:
|
||||
# EmptyTitle is used to prevent the cms from trying
|
||||
# to find a translation in the database
|
||||
if language in titles_by_language:
|
||||
title = titles_by_language[language]
|
||||
indent = " " * (page.node.depth - 1)
|
||||
label = mark_safe("%s%s" % (indent, escape(title)))
|
||||
yield (page.pk, label)
|
||||
break
|
||||
|
||||
|
||||
def update_site_and_page_choices(language=None):
|
||||
if language is None:
|
||||
language = i18n.get_current_language()
|
||||
|
||||
site_choices = []
|
||||
page_choices = [('', '----')]
|
||||
site_choices_key = _site_cache_key(language)
|
||||
page_choices_key = _page_cache_key(language)
|
||||
|
||||
for site in get_sites():
|
||||
_page_choices = list(get_page_choices_for_site(site, language))
|
||||
site_choices.append((site.pk, site.name))
|
||||
page_choices.append((site.name, _page_choices))
|
||||
|
||||
from django.core.cache import cache
|
||||
# We set it to 1 day here because we actively invalidate this cache.
|
||||
cache.set(site_choices_key, site_choices, 86400)
|
||||
cache.set(page_choices_key, page_choices, 86400)
|
||||
return site_choices, page_choices
|
||||
|
||||
|
||||
def get_site_choices(lang=None):
|
||||
from django.core.cache import cache
|
||||
lang = lang or i18n.get_current_language()
|
||||
site_choices = cache.get(_site_cache_key(lang))
|
||||
if site_choices is None:
|
||||
site_choices = update_site_and_page_choices(lang)[0]
|
||||
return site_choices
|
||||
|
||||
|
||||
def get_page_choices(lang=None):
|
||||
from django.core.cache import cache
|
||||
lang = lang or i18n.get_current_language()
|
||||
page_choices = cache.get(_page_cache_key(lang))
|
||||
if page_choices is None:
|
||||
page_choices = update_site_and_page_choices(lang)[1]
|
||||
return page_choices
|
||||
|
||||
|
||||
post_save.connect(clean_page_choices_cache, sender=Page)
|
||||
post_save.connect(clean_site_choices_cache, sender=Site)
|
||||
post_delete.connect(clean_page_choices_cache, sender=Page)
|
||||
post_delete.connect(clean_site_choices_cache, sender=Site)
|
||||
@@ -1,70 +0,0 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator, URLValidator
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext
|
||||
|
||||
from cms.utils.page import get_all_pages_from_path
|
||||
from cms.utils.urlutils import admin_reverse, relative_url_regex
|
||||
|
||||
|
||||
def validate_relative_url(value):
|
||||
RegexValidator(regex=relative_url_regex)(value)
|
||||
|
||||
|
||||
def validate_url(value):
|
||||
try:
|
||||
# Validate relative urls first
|
||||
validate_relative_url(value)
|
||||
except ValidationError:
|
||||
# Fallback to absolute urls
|
||||
URLValidator()(value)
|
||||
|
||||
|
||||
def validate_url_uniqueness(site, path, language, exclude_page=None):
|
||||
""" Checks for conflicting urls
|
||||
"""
|
||||
if '/' in path:
|
||||
validate_url(path)
|
||||
|
||||
path = path.strip('/')
|
||||
pages = get_all_pages_from_path(site, path, language)
|
||||
pages = pages.select_related('publisher_public')
|
||||
|
||||
if exclude_page:
|
||||
pages = pages.exclude(pk=exclude_page.pk)
|
||||
|
||||
if exclude_page.publisher_public_id:
|
||||
pages = pages.exclude(pk=exclude_page.publisher_public_id)
|
||||
|
||||
try:
|
||||
conflict_page = pages[0]
|
||||
except IndexError:
|
||||
return True
|
||||
|
||||
if conflict_page.publisher_is_draft:
|
||||
page_id = conflict_page.pk
|
||||
else:
|
||||
# rare case where draft points to one url
|
||||
# and live points to another which conflicts.
|
||||
# Use the draft ID because public page is not editable.
|
||||
page_id = conflict_page.publisher_public_id
|
||||
|
||||
if conflict_page.is_page_type:
|
||||
change_url = admin_reverse('cms_pagetype_change', args=[page_id])
|
||||
else:
|
||||
change_url = admin_reverse('cms_page_change', args=[page_id])
|
||||
|
||||
conflict_url = '<a href="%(change_url)s" target="_blank">%(page_title)s</a>' % {
|
||||
'change_url': change_url,
|
||||
'page_title': force_text(conflict_page),
|
||||
}
|
||||
|
||||
if exclude_page:
|
||||
message = ugettext('Page %(conflict_page)s has the same url \'%(url)s\' as current page "%(instance)s".')
|
||||
else:
|
||||
message = ugettext('Page %(conflict_page)s has the same url \'%(url)s\' as current page.')
|
||||
message = message % {'conflict_page': conflict_url, 'url': path, 'instance': exclude_page}
|
||||
raise ValidationError(mark_safe(message))
|
||||
@@ -1,262 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import get_permission_codename
|
||||
from django.contrib.sites.models import Site
|
||||
from django.forms.widgets import MultiWidget, Select, TextInput
|
||||
from django.urls import NoReverseMatch, reverse_lazy
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import escape, escapejs
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from cms.utils.urlutils import admin_reverse, static_with_version
|
||||
from cms.forms.utils import get_site_choices, get_page_choices
|
||||
from cms.models import Page, PageUser
|
||||
|
||||
|
||||
class PageSelectWidget(MultiWidget):
|
||||
"""A widget that allows selecting a page by first selecting a site and then
|
||||
a page on that site in a two step process.
|
||||
"""
|
||||
template_name = 'cms/widgets/pageselectwidget.html'
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.pageselectwidget.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, site_choices=None, page_choices=None, attrs=None):
|
||||
if attrs is not None:
|
||||
self.attrs = attrs.copy()
|
||||
else:
|
||||
self.attrs = {}
|
||||
self.choices = []
|
||||
super(PageSelectWidget, self).__init__((Select, Select, Select), attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
"""
|
||||
receives a page_id in value and returns the site_id and page_id
|
||||
of that page or the current site_id and None if no page_id is given.
|
||||
"""
|
||||
if value:
|
||||
page = Page.objects.select_related('node').get(pk=value)
|
||||
return [page.node.site_id, page.pk, page.pk]
|
||||
site = Site.objects.get_current()
|
||||
return [site.pk,None,None]
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
# THIS IS A COPY OF django.forms.widgets.Widget._has_changed()
|
||||
# (except for the first if statement)
|
||||
|
||||
"""
|
||||
Return True if data differs from initial.
|
||||
"""
|
||||
# For purposes of seeing whether something has changed, None is
|
||||
# the same as an empty string, if the data or inital value we get
|
||||
# is None, replace it w/ u''.
|
||||
if data is None or (len(data)>=2 and data[1] in [None,'']):
|
||||
data_value = u''
|
||||
else:
|
||||
data_value = data
|
||||
if initial is None:
|
||||
initial_value = u''
|
||||
else:
|
||||
initial_value = initial
|
||||
if force_text(initial_value) != force_text(data_value):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _build_widgets(self):
|
||||
site_choices = get_site_choices()
|
||||
page_choices = get_page_choices()
|
||||
self.site_choices = site_choices
|
||||
self.choices = page_choices
|
||||
self.widgets = (Select(choices=site_choices ),
|
||||
Select(choices=[('', '----')]),
|
||||
Select(choices=self.choices, attrs={'style': "display:none;"} ),
|
||||
)
|
||||
|
||||
def _build_script(self, name, value, attrs={}):
|
||||
return r"""<script type="text/javascript">
|
||||
var CMS = window.CMS || {};
|
||||
|
||||
CMS.Widgets = CMS.Widgets || {};
|
||||
CMS.Widgets._pageSelectWidgets = CMS.Widgets._pageSelectWidgets || [];
|
||||
CMS.Widgets._pageSelectWidgets.push({
|
||||
name: '%(name)s'
|
||||
});
|
||||
</script>""" % {
|
||||
'name': name
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
self._build_widgets()
|
||||
context = super(PageSelectWidget, self).get_context(name, value, attrs)
|
||||
context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs'])
|
||||
return context
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
return u' '.join(rendered_widgets)
|
||||
|
||||
|
||||
class PageSmartLinkWidget(TextInput):
|
||||
template_name = 'cms/widgets/pagesmartlinkwidget.html'
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': (
|
||||
'cms/js/select2/select2.css',
|
||||
'cms/js/select2/select2-bootstrap.css',
|
||||
)
|
||||
}
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.pagesmartlinkwidget.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, attrs=None, ajax_view=None):
|
||||
super(PageSmartLinkWidget, self).__init__(attrs)
|
||||
self.ajax_url = self.get_ajax_url(ajax_view=ajax_view)
|
||||
|
||||
def get_ajax_url(self, ajax_view):
|
||||
try:
|
||||
return reverse_lazy(ajax_view)
|
||||
except NoReverseMatch:
|
||||
raise Exception(
|
||||
'You should provide an ajax_view argument that can be reversed to the PageSmartLinkWidget'
|
||||
)
|
||||
|
||||
def _build_script(self, name, value, attrs={}):
|
||||
return r"""<script type="text/javascript">
|
||||
var CMS = window.CMS || {};
|
||||
|
||||
CMS.Widgets = CMS.Widgets || {};
|
||||
CMS.Widgets._pageSmartLinkWidgets = CMS.Widgets._pageSmartLinkWidgets || [];
|
||||
CMS.Widgets._pageSmartLinkWidgets.push({
|
||||
id: '%(element_id)s',
|
||||
text: '%(placeholder_text)s',
|
||||
lang: '%(language_code)s',
|
||||
url: '%(ajax_url)s'
|
||||
});
|
||||
</script>""" % {
|
||||
'element_id': attrs.get('id', ''),
|
||||
'placeholder_text': attrs.get('placeholder_text', ''),
|
||||
'language_code': self.language,
|
||||
'ajax_url': force_text(self.ajax_url)
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super(PageSmartLinkWidget, self).get_context(name, value, attrs)
|
||||
context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs'])
|
||||
return context
|
||||
|
||||
|
||||
class UserSelectAdminWidget(Select):
|
||||
"""Special widget used in page permission inlines, because we have to render
|
||||
an add user (plus) icon, but point it somewhere else - to special user creation
|
||||
view, which is accessible only if user haves "add user" permissions.
|
||||
|
||||
Current user should be assigned to widget in form constructor as an user
|
||||
attribute.
|
||||
"""
|
||||
def render(self, name, value, attrs=None, choices=(), renderer=None):
|
||||
output = [super(UserSelectAdminWidget, self).render(name, value, attrs, renderer=renderer)]
|
||||
if hasattr(self, 'user') and (self.user.is_superuser or \
|
||||
self.user.has_perm(PageUser._meta.app_label + '.' + get_permission_codename('add', PageUser._meta))):
|
||||
# append + icon
|
||||
add_url = admin_reverse('cms_pageuser_add')
|
||||
output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
|
||||
(add_url, name))
|
||||
return mark_safe(u''.join(output))
|
||||
|
||||
|
||||
class AppHookSelect(Select):
|
||||
|
||||
"""Special widget used for the App Hook selector in the Advanced Settings
|
||||
of the Page Admin. It adds support for a data attribute per option and
|
||||
includes supporting JS into the page.
|
||||
"""
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.apphookselect.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, attrs=None, choices=(), app_namespaces={}):
|
||||
self.app_namespaces = app_namespaces
|
||||
super(AppHookSelect, self).__init__(attrs, choices)
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
option = super(AppHookSelect, self).create_option(name, value, label, selected, index, subindex, attrs)
|
||||
if value in self.app_namespaces:
|
||||
option['attrs']['data-namespace'] = escape(self.app_namespaces[value])
|
||||
return option
|
||||
|
||||
def _build_option(self, selected_choices, option_value, option_label):
|
||||
if option_value is None:
|
||||
option_value = ''
|
||||
option_value = force_text(option_value)
|
||||
if option_value in selected_choices:
|
||||
selected_html = mark_safe(' selected="selected"')
|
||||
if not self.allow_multiple_selected:
|
||||
# Only allow for a single selection.
|
||||
selected_choices.remove(option_value)
|
||||
else:
|
||||
selected_html = ''
|
||||
|
||||
if option_value in self.app_namespaces:
|
||||
data_html = mark_safe(' data-namespace="%s"' % escape(self.app_namespaces[option_value]))
|
||||
else:
|
||||
data_html = ''
|
||||
return option_value, selected_html, data_html, force_text(option_label)
|
||||
|
||||
def render_option(self, selected_choices, option_value, option_label):
|
||||
option_data = self._build_option(selected_choices, option_value, option_label)
|
||||
return '<option value="%s"%s%s>%s</option>' % option_data
|
||||
|
||||
|
||||
class ApplicationConfigSelect(Select):
|
||||
"""
|
||||
Special widget -populate by javascript- that shows application configurations
|
||||
depending on selected Apphooks.
|
||||
|
||||
Required data are injected in the page as javascript data that cms.app_hook_select.js
|
||||
uses to create the appropriate data structure.
|
||||
|
||||
A stub 'add-another' link is created and filled in with the correct URL by the same
|
||||
javascript.
|
||||
"""
|
||||
template_name = 'cms/widgets/applicationconfigselect.html'
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.apphookselect.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, attrs=None, choices=(), app_configs={}):
|
||||
self.app_configs = app_configs
|
||||
super(ApplicationConfigSelect, self).__init__(attrs, choices)
|
||||
|
||||
def _build_script(self, name, value, attrs={}):
|
||||
configs = []
|
||||
urls = []
|
||||
for application, cms_app in self.app_configs.items():
|
||||
configs.append("'%s': [%s]" % (application, ",".join(
|
||||
["['%s', '%s']" % (config.pk, escapejs(escape(config))) for config in cms_app.get_configs()]))) # noqa
|
||||
for application, cms_app in self.app_configs.items():
|
||||
urls.append("'%s': '%s'" % (application, cms_app.get_config_add_url()))
|
||||
return r"""<script type="text/javascript">
|
||||
var apphooks_configuration = {
|
||||
%(apphooks_configurations)s
|
||||
};
|
||||
var apphooks_configuration_url = {
|
||||
%(apphooks_url)s
|
||||
};
|
||||
var apphooks_configuration_value = '%(apphooks_value)s';
|
||||
</script>""" % {
|
||||
'apphooks_configurations': ','.join(configs),
|
||||
'apphooks_url': ','.join(urls),
|
||||
'apphooks_value': value,
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super(ApplicationConfigSelect, self).get_context(name, value, attrs)
|
||||
context['widget']['script_init'] = self._build_script(name, value, context['widget']['attrs'])
|
||||
return context
|
||||
@@ -1,200 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import (
|
||||
ugettext,
|
||||
ugettext_lazy as _,
|
||||
)
|
||||
|
||||
from cms.admin.forms import AddPageForm
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils import get_current_site, permissions
|
||||
from cms.utils.page import get_available_slug
|
||||
from cms.utils.page_permissions import (
|
||||
user_can_add_page,
|
||||
user_can_add_subpage,
|
||||
)
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.urlutils import static_with_version
|
||||
|
||||
try:
|
||||
# djangocms_text_ckeditor is not guaranteed to be available
|
||||
from djangocms_text_ckeditor.widgets import TextEditorWidget
|
||||
text_widget = TextEditorWidget
|
||||
except ImportError:
|
||||
text_widget = forms.Textarea
|
||||
|
||||
|
||||
class SlugWidget(forms.widgets.TextInput):
|
||||
"""
|
||||
Special widget for the slug field that requires Title field to be there.
|
||||
Adds the js for the slugifying.
|
||||
"""
|
||||
class Media:
|
||||
js = (
|
||||
'admin/js/urlify.js',
|
||||
static_with_version('cms/js/dist/bundle.forms.slugwidget.min.js'),
|
||||
)
|
||||
|
||||
|
||||
class CreateCMSPageForm(AddPageForm):
|
||||
page = None
|
||||
sub_page_form = False
|
||||
|
||||
# Field overrides
|
||||
menu_title = None
|
||||
page_title = None
|
||||
meta_description = None
|
||||
|
||||
content = forms.CharField(
|
||||
label=_(u'Content'), widget=text_widget, required=False,
|
||||
help_text=_(u"Optional. If supplied, will be automatically added "
|
||||
u"within a new text plugin.")
|
||||
)
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
# This simply adds some JS for
|
||||
# hiding/showing the content field based on the selection of this select.
|
||||
'cms/js/widgets/wizard.pagetypeselect.js',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._site = get_current_site()
|
||||
self._user = self.user
|
||||
self._language = self.language_code
|
||||
super(CreateCMSPageForm, self).__init__(*args, **kwargs)
|
||||
self.fields['title'].help_text = _(u"Provide a title for the new page.")
|
||||
self.fields['slug'].required = False
|
||||
self.fields['slug'].widget = SlugWidget()
|
||||
self.fields['slug'].help_text = _(u"Leave empty for automatic slug, or override as required.")
|
||||
|
||||
@staticmethod
|
||||
def get_placeholder(page, slot=None):
|
||||
"""
|
||||
Returns the named placeholder or, if no «slot» provided, the first
|
||||
editable, non-static placeholder or None.
|
||||
"""
|
||||
placeholders = page.get_placeholders()
|
||||
|
||||
if slot:
|
||||
placeholders = placeholders.filter(slot=slot)
|
||||
|
||||
for ph in placeholders:
|
||||
if not ph.is_static and ph.is_editable:
|
||||
return ph
|
||||
|
||||
return None
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validates that either the slug is provided, or that slugification from
|
||||
`title` produces a valid slug.
|
||||
:return:
|
||||
"""
|
||||
data = self.cleaned_data
|
||||
|
||||
if self._errors:
|
||||
return data
|
||||
|
||||
slug = data.get('slug') or slugify(data['title'])
|
||||
|
||||
parent_node = data.get('parent_node')
|
||||
|
||||
if parent_node:
|
||||
base = parent_node.item.get_path(self._language)
|
||||
path = u'%s/%s' % (base, slug) if base else slug
|
||||
else:
|
||||
base = ''
|
||||
path = slug
|
||||
|
||||
data['slug'] = get_available_slug(self._site, path, self._language, suffix=None)
|
||||
data['path'] = '%s/%s' % (base, data['slug']) if base else data['slug']
|
||||
|
||||
if not data['slug']:
|
||||
raise forms.ValidationError("Please provide a valid slug.")
|
||||
return data
|
||||
|
||||
def clean_parent_node(self):
|
||||
# Check to see if this user has permissions to make this page. We've
|
||||
# already checked this when producing a list of wizard entries, but this
|
||||
# is to prevent people from possible form-hacking.
|
||||
if self.page and self.sub_page_form:
|
||||
# User is adding a page which will be a direct
|
||||
# child of the current page.
|
||||
parent_page = self.page
|
||||
elif self.page and self.page.parent_page:
|
||||
# User is adding a page which will be a right
|
||||
# sibling to the current page.
|
||||
parent_page = self.page.parent_page
|
||||
else:
|
||||
parent_page = None
|
||||
|
||||
if parent_page:
|
||||
has_perm = user_can_add_subpage(self.user, target=parent_page)
|
||||
else:
|
||||
has_perm = user_can_add_page(self.user)
|
||||
|
||||
if not has_perm:
|
||||
message = ugettext('You don\'t have the permissions required to add a page.')
|
||||
raise ValidationError(message)
|
||||
return parent_page.node if parent_page else None
|
||||
|
||||
def clean_slug(self):
|
||||
# Don't let the PageAddForm validate this
|
||||
# on the wizard it is not a required field
|
||||
return self.cleaned_data['slug']
|
||||
|
||||
def get_template(self):
|
||||
return get_cms_setting('PAGE_WIZARD_DEFAULT_TEMPLATE')
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, **kwargs):
|
||||
from cms.api import add_plugin
|
||||
|
||||
new_page = super(CreateCMSPageForm, self).save(**kwargs)
|
||||
|
||||
if self.cleaned_data.get("page_type"):
|
||||
return new_page
|
||||
|
||||
parent_node = self.cleaned_data.get('parent_node')
|
||||
|
||||
if parent_node and new_page.parent_page.is_page_type:
|
||||
# the new page was created under a page-type page
|
||||
# set the new page as a page-type too
|
||||
new_page.update(
|
||||
draft_only=True,
|
||||
is_page_type=True,
|
||||
in_navigation=False,
|
||||
)
|
||||
|
||||
# If the user provided content, then use that instead.
|
||||
content = self.cleaned_data.get('content')
|
||||
plugin_type = get_cms_setting('PAGE_WIZARD_CONTENT_PLUGIN')
|
||||
plugin_body = get_cms_setting('PAGE_WIZARD_CONTENT_PLUGIN_BODY')
|
||||
slot = get_cms_setting('PAGE_WIZARD_CONTENT_PLACEHOLDER')
|
||||
|
||||
if plugin_type in plugin_pool.plugins and plugin_body:
|
||||
if content and permissions.has_plugin_permission(
|
||||
self.user, plugin_type, "add"):
|
||||
new_page.rescan_placeholders()
|
||||
placeholder = self.get_placeholder(new_page, slot=slot)
|
||||
if placeholder:
|
||||
opts = {
|
||||
'placeholder': placeholder,
|
||||
'plugin_type': plugin_type,
|
||||
'language': self.language_code,
|
||||
plugin_body: content,
|
||||
}
|
||||
add_plugin(**opts)
|
||||
return new_page
|
||||
|
||||
|
||||
class CreateCMSSubPageForm(CreateCMSPageForm):
|
||||
|
||||
sub_page_form = True
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,26 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Abdelkader Maarouf <dep2g2@gmail.com>, 2015
|
||||
# Ahmed H <info@draco-003.com>, 2013
|
||||
# Bashar Al-Abdulhadi, 2017
|
||||
# Bashar Ghadanfar <10.tens@gmail.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Arabic (http://www.transifex.com/divio/django-cms/language/ar/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ar\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "هل أنت متأكد من الإنتقال الى تبويب أخر دون حفظ الصفحة أولاً؟"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Boris Chervenkov <office@sentido.bg>, 2012
|
||||
# Boyko Amarov <boyko.amarov@gmail.com>, 2013
|
||||
# suleyman <posta@suleymans.com>, 2011
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Bulgarian (http://www.transifex.com/divio/django-cms/language/bg/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: bg\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Наистина ли искате да промените раздели без първо да запазите страницата?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Aniruddha Adhikary <aniruddha@adhikary.net>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Bengali (http://www.transifex.com/divio/django-cms/language/bn/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: bn\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "আপনি কি নিশ্চিত যে, আপনি পৃষ্ঠাটি সংরক্ষণ না করেই ট্যাব পরিবর্তন করতে চান?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Roger Pons <rogerpons@gmail.com>, 2012,2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Catalan (http://www.transifex.com/divio/django-cms/language/ca/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ca\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Esteu segur que voleu canviar de secció sense guardar la primera pàgina?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Jakub Dorňák <jakub.dornak@misli.cz>, 2013
|
||||
# xlu <xlu@seznam.cz>, 2013
|
||||
# xlu <xlu@seznam.cz>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Czech (http://www.transifex.com/divio/django-cms/language/cs/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: cs\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Určitě chcete změnit záložku bez uložení změn?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Kristian Øllegaard <kristian@oellegaard.com>, 2011
|
||||
# Kristian Øllegaard <kristian@oellegaard.com>, 2011
|
||||
# Kristian Øllegaard <kristian@oellegaard.com>, 2011
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Danish (http://www.transifex.com/divio/django-cms/language/da/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: da\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Er du sikker på du vil ændre tabs uden at gemme siden først?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,25 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# Jonas Obrist <me@ojii.ch>, 2011
|
||||
# Michael P. Jung <michael.jung@terreon.de>, 2013
|
||||
# Stefan T. Oertel <me@schneck.cc>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: German (http://www.transifex.com/divio/django-cms/language/de/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: de\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Sind Sie sicher Sie wollen die Sprache ändern ohne vorher zu speichern?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
# George Petsagourakis <petsagouris@gmail.com>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: Greek (http://www.transifex.com/divio/django-cms/language/el/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: el\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Είσαι σίγουρος πως θες να αλλάξεις καρτέλες χωρίς να έχεις αποθηκεύσει πρώτα τη σελίδα?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Jonas Obrist <ojiidotch@gmail.com>, 2011
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-cms\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2015-10-31 12:28+0000\n"
|
||||
"Last-Translator: yakky <i.spalletti@nephila.it>\n"
|
||||
"Language-Team: English (http://www.transifex.com/divio/django-cms/language/"
|
||||
"en/)\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr "Are you sure you want to change tabs without saving the page first?"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,22 +0,0 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django CMS\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-01-29 08:34+0100\n"
|
||||
"PO-Revision-Date: 2018-01-11 16:38+0000\n"
|
||||
"Last-Translator: Paulo Alvarado <paulo.alvarado@divio.ch>\n"
|
||||
"Language-Team: English (United Kingdom) (http://www.transifex.com/divio/django-cms/language/en_GB/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: en_GB\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Are you sure you want to change tabs without saving the page first?"
|
||||
msgstr ""
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user