├── .coveragerc
├── .gitattributes
├── .gitignore
├── .gitlab-ci.yml
├── .pylintrc
├── .readthedocs.yaml
├── CHANGELOG
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── PITCHME.md
├── PITCHME.yaml
├── README.rst
├── assets
├── css
│ └── PITCHME.css
├── images
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── Banner_Logo_yourLabs_White_13_Copy_9_bkheoj.webp
│ ├── CRUDFLAP_HOME.png
│ ├── Post_create.png
│ ├── YourLabs_Logo_Simple_transparent_wwbd35.webp
│ ├── dir_str.jpeg
│ ├── dir_str_1.png
│ ├── gitpitch-audience.jpg
│ ├── in-60-seconds.jpg
│ ├── post_list.png
│ ├── post_menu.png
│ └── update_post.png
└── sample
│ ├── crudlfap.py
│ ├── models.py
│ ├── settings.py
│ └── urls.py
├── conftest.py
├── docker-compose.persist.yml
├── docker-compose.traefik.yml
├── docker-compose.yml
├── docs
├── Makefile
├── conf.py
├── crudlfap_auth
│ ├── index.rst
│ └── views.rst
├── factory.rst
├── index.rst
├── install.rst
├── requirements.txt
├── route.rst
├── router.rst
├── screenshots
│ ├── add-modal.png
│ ├── detail-template.png
│ ├── list.png
│ └── update-form.png
├── settings.rst
├── tutorial.rst
└── views.rst
├── manage.py
├── nightwatch
├── eslintrc.json
├── globals-driver.js
├── globals.js
├── nightwatch-driver.json
├── nightwatch.conf.js
├── package.json
├── shared
│ ├── CONSTANTS.ts
│ └── commonFunction.ts
├── tests
│ ├── artist
│ │ ├── artistAction.ts
│ │ └── validation.ts
│ ├── group
│ │ ├── groupAction.ts
│ │ └── validation.ts
│ ├── login.ts
│ ├── logout.ts
│ ├── menu.ts
│ ├── post
│ │ ├── postAction.ts
│ │ └── validation.ts
│ ├── songRating
│ │ └── validation.ts
│ ├── songs
│ │ ├── songAction.ts
│ │ └── validation.ts
│ └── user
│ │ ├── userAction.ts
│ │ └── validation.ts
└── tsconfig.json
├── pytest.ini
├── readthedocs.yml
├── setup.py
├── src
├── crudlfap
│ ├── __init__.py
│ ├── apps.py
│ ├── conf.py
│ ├── crudlfap.py
│ ├── factory.py
│ ├── html.py
│ ├── locale
│ │ └── fr
│ │ │ └── LC_MESSAGES
│ │ │ └── django.po
│ ├── management
│ │ ├── __init__.py
│ │ └── commands
│ │ │ └── __init__.py
│ ├── mixins
│ │ ├── __init__.py
│ │ ├── crud.py
│ │ ├── filter.py
│ │ ├── form.py
│ │ ├── lock.py
│ │ ├── menu.py
│ │ ├── model.py
│ │ ├── modelform.py
│ │ ├── object.py
│ │ ├── objectform.py
│ │ ├── objects.py
│ │ ├── objectsform.py
│ │ ├── search.py
│ │ ├── table.py
│ │ └── template.py
│ ├── models.py
│ ├── registry.py
│ ├── route.py
│ ├── router.py
│ ├── settings.py
│ ├── shortcuts.py
│ ├── site.py
│ ├── static
│ │ ├── swagger-ui-bundle.js
│ │ ├── swagger-ui-standalone-preset.js
│ │ └── swagger-ui.css
│ ├── test_conf.py
│ ├── test_factory.py
│ ├── test_route.py
│ ├── test_router.py
│ ├── utils.py
│ └── views
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── generic.py
├── crudlfap_auth
│ ├── __init__.py
│ ├── backends.py
│ ├── crudlfap.py
│ ├── html.py
│ ├── locale
│ │ └── fr
│ │ │ └── LC_MESSAGES
│ │ │ └── django.po
│ ├── models.py
│ ├── test_urls.py
│ └── views.py
├── crudlfap_example
│ ├── __init__.py
│ ├── artist
│ │ ├── __init__.py
│ │ ├── crudlfap.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ └── test_api.py
│ ├── blog
│ │ ├── crudlfap.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ └── test_security.py
│ ├── manage.py
│ ├── settings.py
│ ├── song
│ │ ├── __init__.py
│ │ ├── crudlfap.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ └── test_security.py
│ ├── urls.py
│ └── wsgi.py
├── crudlfap_registration
│ ├── __init__.py
│ ├── crudlfap.py
│ ├── html.py
│ ├── templates
│ │ └── django_registration
│ │ │ ├── activation_email_body.txt
│ │ │ └── activation_email_subject.txt
│ └── test_registration.py
└── crudlfap_sites
│ ├── __init__.py
│ ├── crudlfap.py
│ ├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_site_settings.py
│ └── __init__.py
│ └── models.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | omit = .tox
4 |
5 | [report]
6 | # Regexes for lines to exclude from consideration
7 | exclude_lines =
8 | # Have to re-enable the standard pragma
9 | pragma: no cover
10 |
11 | # Don't complain about missing debug-only code:
12 | def __repr__
13 | if self\.debug
14 |
15 | # Don't complain if tests don't hit defensive assertion code:
16 | raise AssertionError
17 | raise NotImplementedError
18 |
19 | # Don't complain if non-runnable code isn't run:
20 | if 0:
21 | if __name__ == .__main__.:
22 |
23 | ignore_errors = True
24 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | src/crudlfap/static/* binary
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | variables:
2 | ANSIBLE_FORCE_COLOR: 'true'
3 | RELEASE_TAG: yourlabs/crudlfap:$CI_COMMIT_REF_NAME
4 | IMAGE_TAG: yourlabs/crudlfap:$CI_COMMIT_SHA
5 |
6 | build:
7 | stage: build
8 | image: docker:stable
9 | before_script:
10 | - echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin
11 | script:
12 | - docker pull yourlabs/crudlfap:master || true
13 | - docker build -t $IMAGE_TAG .
14 | - docker push $IMAGE_TAG
15 | after_script:
16 | - docker logout
17 |
18 | build-test:
19 | image:
20 | name: $IMAGE_TAG
21 | entrypoint: ["/bin/sh", "-c"]
22 | stage: test
23 | script:
24 | - pip install -e .[project]
25 | - pip install pytest pytest-cov pytest-asyncio pytest-django
26 | - pytest -vv --cov src --cov-report=xml:coverage.xml --cov-report=term-missing --strict -r fEsxXw src
27 |
28 | .deploy: &deploy
29 | before_script:
30 | - mkdir -p ~/.ssh; echo "$CI_SSH_KEY" > ~/.ssh/id_ed25519; echo "$SSH_FINGERPRINTS" > ~/.ssh/known_hosts; chmod 700 ~/.ssh; chmod 600 ~/.ssh/*
31 | script:
32 | - set -x
33 | - export HOST=$(echo $CI_ENVIRONMENT_URL | sed s@^.*://@@)
34 | - export PROTO=$(echo $CI_ENVIRONMENT_URL | sed s@:.*@@)
35 | - export CI_PROJECT_SLUG=$(echo $CI_PROJECT_NAME | sed s@[/.]@-@g)
36 | - export ANSIBLE_HOST_KEY_CHECKING=False
37 | - bigsudo yourlabs.compose
38 | compose_django_build=
39 | compose_django_image=$IMAGE_TAG
40 | wait_grep=uwsgi
41 | pull=no
42 | $DEPLOY
43 | crudlfap@ci.yourlabs.io
44 | | tee deploy.log
45 | - grep unreachable=0 deploy.log &> /dev/null
46 | - grep failed=0 deploy.log &> /dev/null
47 |
48 | review-deploy:
49 | image: yourlabs/ansible
50 | stage: test
51 | environment:
52 | name: test/$CI_COMMIT_REF_NAME
53 | url: http://${CI_ENVIRONMENT_SLUG}.crudlfap.ci.yourlabs.io
54 | variables:
55 | DEPLOY: >
56 | compose=docker-compose.yml,docker-compose.traefik.yml
57 | lifetime=86400
58 | project=$CI_ENVIRONMENT_SLUG
59 | except:
60 | - tags
61 | - master
62 | <<: *deploy
63 |
64 | master-deploy:
65 | image: yourlabs/ansible
66 | stage: deploy
67 | environment:
68 | name: master
69 | url: https://master.crudlfap.ci.yourlabs.io
70 | variables:
71 | DEPLOY: >
72 | compose=docker-compose.yml,docker-compose.traefik.yml,docker-compose.persist.yml
73 | home=/home/crudlfap-master
74 | only:
75 | refs:
76 | - master
77 | <<: *deploy
78 |
79 | demo:
80 | image: yourlabs/ansible
81 | stage: deploy
82 | environment:
83 | name: demo
84 | url: https://demo.crudlfap.ci.yourlabs.io
85 | variables:
86 | DEPLOY: >
87 | compose=docker-compose.yml,docker-compose.traefik.yml,docker-compose.persist.yml
88 | home=/home/crudlfap-demo
89 | only: [tags]
90 | <<: *deploy
91 |
92 | docs:
93 | stage: build
94 | image: yourlabs/python-arch
95 | artifacts:
96 | expire_in: 2 days
97 | when: always
98 | paths: [public]
99 | script:
100 | - pip install -r docs/requirements.txt
101 | - pushd docs && make html && popd
102 | - mv docs/_build/html public
103 |
104 | py-qa:
105 | stage: build
106 | image: yourlabs/python-arch
107 | script: tox -e qa
108 |
109 | py-test:
110 | stage: build
111 | image: yourlabs/python-arch
112 | script:
113 | - tox -e py310-dj40
114 | - codecov-bash -e TOXENV -f coverage.xml
115 |
116 | pypi:
117 | stage: deploy
118 | image: yourlabs/python-arch
119 | script: pypi-release
120 | only: [tags]
121 |
122 | pages:
123 | stage: test
124 | image: alpine:latest
125 | artifacts:
126 | paths: [public]
127 | only:
128 | - master
129 | script: find public
130 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [DESIGN]
2 | max-parents=15
3 | disable=invalid-name
4 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | build:
2 | image: testing
3 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | 0.7.0
2 |
3 | NEW FEATURES
4 |
5 | - Non-db model support has been greatly improved, using the lookupy module that
6 | implements a QuerySet interface for python objects, we just added support for
7 | models that have managed=False,
8 | - You can now browse your list of registered `Routers` and `Routes` in
9 | CRUDLFA+, these are auto-generated during runtime into
10 | `crudlfap.models.Controller` and `crudlfap.models.URL`,
11 | - By doing so, you can authorize groups that should have the view permission in
12 | each view's detail page, refer to the new documentation about security model
13 | for details,
14 | - As such, the poor urls debug view is gone in favor of the above,
15 | - Django permission API is now the default way of dealing with permission,
16 | - BC Breaks listed below
17 |
18 | BACKWARD COMPATIBILITY BREAKS
19 |
20 | So, basically this will make your 0.5 CRUDLFA+ project start correctly on 0.6::
21 |
22 | find src/ -type f | xargs sed -i 's/from crudlfap import crudlfap/from crudlfap import shortcuts as crudlfap/'
23 |
24 | BUT all your views will be invisible to non-superusers. Follow these steps to
25 | upgrade:
26 |
27 | - `from crudlfap import crudlfap` should now be `from crudlfap import shortcuts
28 | as crudlfap`,
29 | - `allowed` is gone, in favor of `has_perm()`, that checks django permission by
30 | default, that means views are not open to staff users by default but to
31 | superusers
32 | - as such, to open a view to all, replace `allowed=True` with
33 | `authenticate=False`,
34 | - `Router.get_objects_for_user(user, perms)` is gone in favor of
35 | `Router.get_queryset(view)`, which returns all models by default.
36 | - `Router.get_fields_for_user(user, perms, obj=None)` becomes
37 | `Router.get_fields(view)`,
38 | - `Route.short_permission_code` becomes `Route.permission_shortcode`,
39 | - `Route.full_permission_code` becomes `Route.permission_fullcode`,
40 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM archlinux
2 |
3 | ENV DJANGO_SETTINGS_MODULE=crudlfap_example.settings
4 | ENV UWSGI_MODULE=crudlfap_example.wsgi:application
5 |
6 | ENV NODE_ENV=production
7 | ENV PATH="${PATH}:/app/.local/bin"
8 | ENV PYTHONIOENCODING=UTF-8 PYTHONUNBUFFERED=1 STATIC_ROOT=/app/public
9 | EXPOSE 8000
10 |
11 | RUN pacman -Syu --noconfirm mailcap which gettext python python-pillow python-psycopg2 python-pip python-psutil git curl uwsgi uwsgi-plugin-python python python-hiredis libsass && pip install --break-system-packages --upgrade pip djcli
12 | RUN useradd --home-dir /app --uid 1000 app && mkdir -p /app && chown -R app /app
13 | WORKDIR /app
14 |
15 | COPY setup.py README.rst MANIFEST.in /app/
16 | COPY src /app/src
17 | COPY manage.py /app
18 | RUN pip install --break-system-packages --editable /app[project]
19 |
20 | RUN ./manage.py ryzom_bundle
21 | RUN DEBUG=1 ./manage.py collectstatic --noinput
22 | RUN find public -type f | xargs gzip -f -k -9
23 |
24 | USER app
25 |
26 | ARG GIT_COMMIT
27 | ARG GIT_TAG
28 | ENV GIT_COMMIT="${GIT_COMMIT}" GIT_TAG="${GIT_TAG}"
29 |
30 | CMD bash -c "djcli dbcheck && ./manage.py migrate --noinput && uwsgi \
31 | --http-socket=0.0.0.0:8000 \
32 | --chdir=/app \
33 | --plugin=python \
34 | --module=${UWSGI_MODULE} \
35 | --http-keepalive \
36 | --harakiri=120 \
37 | --max-requests=100 \
38 | --master \
39 | --workers=8 \
40 | --processes=4 \
41 | --chmod=666 \
42 | --log-5xx \
43 | --vacuum \
44 | --enable-threads \
45 | --post-buffering=8192 \
46 | --ignore-sigpipe \
47 | --ignore-write-errors \
48 | --disable-write-exception \
49 | --mime-file /etc/mime.types \
50 | --route '^/static/.* addheader:Cache-Control: public, max-age=7776000' \
51 | --route '^/js|css|fonts|images|icons|favicon.png/.* addheader:Cache-Control: public, max-age=7776000' \
52 | --static-map /static=/app/public \
53 | --static-map /media=/app/media \
54 | --static-gzip-all"
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 YourLabs. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 | 2. 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.
7 | 3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
8 | This product includes software developed by the organization.
9 | 4. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10 | THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "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 COPYRIGHT HOLDER 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.
11 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.rst LICENSE
2 | recursive-include src *.html *.css *.js *.py *.po *.mo *.txt
3 |
--------------------------------------------------------------------------------
/PITCHME.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | @title[CRUDLFA]
4 |
5 | 
6 |
7 | ## @color[#DC143C](CRUDLFA+)
8 |
9 |
10 | CRUDLFA+ stands for Create Read Update Delete List Form Autocomplete and more.
11 |
12 | ---
13 |
14 | ### @css[crudlfa-headline](prerequisite)
15 |
16 | - pip |
17 | - pip is a package manager for Python packages, or modules if you like. |
18 | - Virtualenv |
19 | - virtualenv is a tool to create isolated Python environments. |
20 |
21 | ---
22 | ### @color[#DC143C](Django)
23 | Django is a free and open source web application framework written in Python. A framework is nothing more than a collection of modules that make development easier.
24 |
25 | ---
26 |
27 | ### @color[#DC143C](Installation of Django)
28 | pip install django
29 |
30 | ---
31 | ### @color[#DC143C](Create Django Project)
32 | - startproject |
33 | django-admin startproject mysite
34 | - Directory structure |
35 | 
36 |
37 | Note:
38 | - This will create a mysite directory in your current directory, and the structure would like this.
39 |
40 | ---
41 | ### @color[#DC143C](Create post application)
42 | - startapp |
43 | django-admin startapp post
44 | - Application Structure |
45 | 
46 |
47 | Note:
48 | - This will create a post application in inside mysite directory.
49 |
50 |
51 | ---
52 | ### @color[#DC143C](default View)
53 | - Run Server |
54 | python manage.py runserver
55 | - You can access your application using the following URL. |
56 | http://127.0.0.1:8000
57 |
58 | Note:
59 | - This is the default view of the django application.
60 | - Now we will learn CRUD operation using CRUDLFAP.
61 |
62 | ---
63 | ### @css[crudlfa-headline](Integration of CRUDLFA+)
64 |
65 | - You can install CRUDLFA+ by the following ways. |
66 | - Installing from pip |
67 | pip install crudlfap
68 | - If you are not in a virtualenv, the above will fail if not executed as root, in this case use |
69 | pip install --user crudlfap
70 |
71 | ---?code=assets/sample/settings.py&lang=python&title=@css[crudlfa-headline](Integration of CRUDLFA+)
72 | @[14-15](import @color[#DC143C](CRUDLFAP_APPS) and @color[#DC143C](CRUDLFAP_TEMPLATE_BACKEND) from @color[#DC143C](crudlfap))
73 | @[34-42](Add @color[#DC143C](CRUDLFAP_APPS) with installed app)
74 | @[57-69](Add the @color[#DC143C](CRUDLFAP_TEMPLATE_BACKEND) line inside TEMPLATES.)
75 |
76 |
77 | ---?code=assets/sample/urls.py&lang=python&title=@css[crudlfa-headline](piece of code we need to add in @color[#DC143C](urls.py) file)
78 | @[18](import @color[#DC143C](crudlfap) from @color[#DC143C](crudlfap))
79 | @[20-23](add @color[#DC143C](crudlfap) urls in urlpatterns)
80 |
81 |
82 | ---
83 | ### Using @color[#DC143C](CRUDLFA+)
84 | - Register custom application with Django |
85 | INSTALLED_APPS = [
86 | .....
87 | 'post',
88 | ]
89 |
90 | Note:
91 | - Now we will register the post application that we created inside settings module.
92 | - We just need to mention this app name inside INSTALLED_APPS list.
93 |
94 | ---?code=assets/sample/models.py&lang=python&title=@color[#DC143C](Models in Djano)
95 |
96 | ---
97 | ### Using @color[#DC143C](CRUDLFA+)
98 | - Register Post model with crudlfap .
99 |
100 | from crudlfap import crudlfap
101 | from .models import Post
102 |
103 | crudlfap.Router(model=Post).register()
104 |
105 | Note:
106 | - Create a file crudlfap.py inside your post application directory with this piece of code.
107 |
108 | ---
109 | ### Using @color[#DC143C](CRUDLFA+)
110 | - makemigrations for post model
111 | python manage.py makemigrations
112 |
113 | Note:
114 | - Now, We need to create post table using @color[#DC143C](makemigrations) command.
115 | - This will create a migration file inside post app migrations directory.
116 |
117 | ---
118 | ### Using @color[#DC143C](CRUDLFA+)
119 | - after that we need to apply migration by following command.
120 | python manage.py migrate
121 |
122 | ---
123 | ### @css[crudlfa-headline](Automatic model menus)
124 | 
125 |
126 | ---
127 | #### @color[#DC143C](Create Post with CRUDLFA+)
128 | 
129 |
130 | ---
131 | #### @color[#DC143C](Create Post pop-up with CRUDLFA+)
132 | 
133 |
134 | ---
135 | #### @color[#DC143C](List Posts with Automatic Object Level Menus)
136 | 
137 |
138 | ---
139 | #### @color[#DC143C](Update View)
140 | 
141 |
142 | ---
143 | #### @color[#DC143C](Delete View)
144 | 
145 |
146 | ---
147 | ### @color[#DC143C](Extend Object Icon)
148 | @css[byline](Change material icon )
149 |
150 | 
151 |
152 | https://material.io/tools/icons/?style=baseline
153 | Note:
154 | - We can extend crudlfap feature, like we can change icon, namespace, we can override views etc.
155 | - We can change the icon by overriding "material_icon" inside router. you can get icon from https://material.io/tools/icons/?style=baseline
156 | - In next slide we shows the code as well to change the icon.
157 |
158 |
159 | ---?code=assets/sample/crudlfap.py&lang=python&title=@css[crudlfa-headline](Extending CRUDLFAP features)
160 | @[7-10](Extending @color[#DC143C](Router))
161 | @[31-33](Change material icon)
162 | @[35-39](Extending List View)
163 | @[33-41](Extending Create View)
164 | @[13-18](Override ModelMixin Class)
165 | @[21-26](Override PostCreateView)
166 |
167 |
168 | Note:
169 | - We can extend crudlfap feature, like we can change icon, namespace, we can override views etc.
170 | - We can change the icon by overriding "material_icon" inside router. you can get icon from https://material.io/tools/icons/?style=baseline
171 | - We can override views like ListView we can set filters, search fields etc.
172 | - We can override CreateView to assign specific user or current user while creating post.
173 |
174 |
175 | ---?image=assets/images/gitpitch-audience.jpg
176 | # @color[#DC143C](Thank-You)
177 |
--------------------------------------------------------------------------------
/PITCHME.yaml:
--------------------------------------------------------------------------------
1 | mathjax : TeX-MML-AM_HTMLorMML-full
2 | theme-override : assets/css/PITCHME.css
3 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | .. image:: https://img.shields.io/readthedocs/crudlfap.svg
2 | :target: https://crudlfap.readthedocs.io
3 | .. image:: https://yourlabs.io/oss/crudlfap/badges/master/build.svg
4 | :target: https://circleci.com/gh/yourlabs/crudlfap
5 | .. image:: https://img.shields.io/codecov/c/github/yourlabs/crudlfap/master.svg
6 | :target: https://codecov.io/gh/yourlabs/crudlfap
7 | .. image:: https://img.shields.io/npm/v/crudlfap.svg
8 | :target: https://www.npmjs.com/package/crudlfap
9 | .. image:: https://img.shields.io/pypi/v/crudlfap.svg
10 | :target: https://pypi.python.org/pypi/crudlfap
11 |
12 | Welcome to CRUDLFA+ for Django 3.0: because Django is FUN !
13 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14 |
15 | CRUDLFA+ stands for Create Read Update Delete List Form Autocomplete and more.
16 |
17 | This plugin for Django makes a rich user interface from Django models, built
18 | with Material Components Web Ryzom Components, offering optionnal databinding
19 | with channels support.
20 |
21 | Demo:
22 |
23 | - last release: https://demo.crudlfap.ci.yourlabs.io/
24 | - current master (might be down/broken etc): https://master.crudlfap.ci.yourlabs.io/
25 |
26 | Try
27 | ===
28 |
29 | This should start the example project from ``src/crudlfap_example`` where each
30 | documented example lives::
31 |
32 | # This installs the repo in ./src/crudlfap and in your python user packages, i run this from ~
33 | pip install --user -e git+https://github.com/yourlabs/crudlfap.git#egg=crudlfap[example]
34 | cd src/crudlfap
35 |
36 | ./manage.py migrate
37 | ./manage.py createsuperuser
38 | ./manage.py runserver
39 |
40 | Features
41 | ========
42 |
43 | - DRY into ModelRouter for all views of a Model,
44 | - extensive CRUD views, actions, etc
45 | - Rich frontend interface out of the box, MDC/Ryzom/Unpoly
46 |
47 | Resources
48 | =========
49 |
50 | - `Documentation
51 | `_
52 | - `ChatRoom graciously hosted by
53 | `_ by `YourLabs Business Service
54 | `_ on `Mattermost
55 | `_
56 | - `Mailing list graciously hosted
57 | `_ by `Google
58 | `_
59 | - For **Security** issues, please contact yourlabs-security@googlegroups.com
60 | - `Git graciously hosted
61 | `_ by `YourLabs Business Service
62 | `_ with `GitLab
63 | `_
64 | - `Package graciously hosted
65 | `_ by `PyPi
66 | `_,
67 | - `Continuous integration graciously hosted
68 | `_ by YourLabs Business Service
69 | - Browser test graciously hosted by `SauceLabs
70 | `_
71 |
--------------------------------------------------------------------------------
/assets/css/PITCHME.css:
--------------------------------------------------------------------------------
1 |
2 | .headline {
3 | color: blue;
4 | text-transform: uppercase;
5 | }
6 |
7 | .byline {
8 | color: gray;
9 | font-size: 0.9em;
10 | text-transform: lowercase;
11 | letter-spacing: 2px;
12 | }
13 | .crudlfa-headline {
14 | color: red;
15 | text-transform: uppercase;
16 | }
--------------------------------------------------------------------------------
/assets/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/1.png
--------------------------------------------------------------------------------
/assets/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/2.png
--------------------------------------------------------------------------------
/assets/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/3.png
--------------------------------------------------------------------------------
/assets/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/4.png
--------------------------------------------------------------------------------
/assets/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/5.png
--------------------------------------------------------------------------------
/assets/images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/6.png
--------------------------------------------------------------------------------
/assets/images/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/7.png
--------------------------------------------------------------------------------
/assets/images/Banner_Logo_yourLabs_White_13_Copy_9_bkheoj.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/Banner_Logo_yourLabs_White_13_Copy_9_bkheoj.webp
--------------------------------------------------------------------------------
/assets/images/CRUDFLAP_HOME.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/CRUDFLAP_HOME.png
--------------------------------------------------------------------------------
/assets/images/Post_create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/Post_create.png
--------------------------------------------------------------------------------
/assets/images/YourLabs_Logo_Simple_transparent_wwbd35.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/YourLabs_Logo_Simple_transparent_wwbd35.webp
--------------------------------------------------------------------------------
/assets/images/dir_str.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/dir_str.jpeg
--------------------------------------------------------------------------------
/assets/images/dir_str_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/dir_str_1.png
--------------------------------------------------------------------------------
/assets/images/gitpitch-audience.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/gitpitch-audience.jpg
--------------------------------------------------------------------------------
/assets/images/in-60-seconds.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/in-60-seconds.jpg
--------------------------------------------------------------------------------
/assets/images/post_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/post_list.png
--------------------------------------------------------------------------------
/assets/images/post_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/post_menu.png
--------------------------------------------------------------------------------
/assets/images/update_post.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/assets/images/update_post.png
--------------------------------------------------------------------------------
/assets/sample/crudlfap.py:
--------------------------------------------------------------------------------
1 | """Extending CRUDLFAP features."""
2 |
3 | from crudlfap import crudlfap
4 |
5 | from .models import Post
6 |
7 | crudlfap.Router(
8 | Post,
9 | views=[]
10 | ).register()
11 |
12 |
13 | class PostMixin:
14 | """Create mixin."""
15 | def get_exclude(self):
16 | if not self.request.user.is_staff:
17 | return ['owner']
18 | return super().get_exclude()
19 |
20 |
21 | class PostCreateView(PostMixin, crudlfap.CreateView):
22 | """Override Post create view."""
23 | def form_valid(self):
24 | """Assigned currnet user."""
25 | self.form.instance.owner = self.request.user
26 | return super().form_valid()
27 |
28 |
29 | crudlfap.Router(
30 | Post,
31 | material_icon='forum',
32 | # https://material.io/tools/icons/?style=baseline
33 | namespace='posts',
34 | views=[
35 | crudlfap.ListView.clone(
36 | filter_fields=['owner'],
37 | search_fields=['name', 'publish', 'owner'],
38 | ),
39 | PostCreateView,
40 | crudlfap.UpdateView,
41 | crudlfap.DeleteView,
42 | ]
43 | ).register()
44 |
--------------------------------------------------------------------------------
/assets/sample/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db import models
3 | from django.utils import timezone
4 |
5 |
6 | class Post(models.Model):
7 | """A Post model with name, description, publish and owner fields."""
8 | name = models.CharField(max_length=100, verbose_name='title')
9 | description = models.TextField(verbose_name='Description')
10 | publish = models.DateTimeField(default=timezone.now)
11 | owner = models.ForeignKey(User, on_delete=models.CASCADE)
12 |
13 | def __str__(self):
14 | """Return string name."""
15 | return self.name
16 |
--------------------------------------------------------------------------------
/assets/sample/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for mysite project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.1.1.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.1/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | from crudlfap.settings import CRUDLFAP_APPS, CRUDLFAP_TEMPLATE_BACKEND
16 |
17 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19 |
20 |
21 | # Quick-start development settings - unsuitable for production
22 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
23 |
24 | # SECURITY WARNING: keep the secret key used in production secret!
25 | SECRET_KEY = '-sfb3y%mgq##s+p%b=o48c(eye+4h05!0zs5lmka^c6+2m8iqc'
26 |
27 | # SECURITY WARNING: don't run with debug turned on in production!
28 | DEBUG = True
29 |
30 | ALLOWED_HOSTS = []
31 |
32 |
33 | # Application definition
34 |
35 | INSTALLED_APPS = [
36 | 'django.contrib.admin',
37 | 'django.contrib.auth',
38 | 'django.contrib.contenttypes',
39 | 'django.contrib.sessions',
40 | 'django.contrib.messages',
41 | 'django.contrib.staticfiles',
42 | 'post',
43 | ] + CRUDLFAP_APPS
44 |
45 |
46 | MIDDLEWARE = [
47 | 'django.middleware.security.SecurityMiddleware',
48 | 'django.contrib.sessions.middleware.SessionMiddleware',
49 | 'django.middleware.common.CommonMiddleware',
50 | 'django.middleware.csrf.CsrfViewMiddleware',
51 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
52 | 'django.contrib.messages.middleware.MessageMiddleware',
53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
54 | ]
55 |
56 | ROOT_URLCONF = 'mysite.urls'
57 |
58 | TEMPLATES = [
59 | {
60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
61 | 'DIRS': [],
62 | 'APP_DIRS': True,
63 | 'OPTIONS': {
64 | 'context_processors': [
65 | '.....'
66 | ],
67 | },
68 | },
69 | CRUDLFAP_TEMPLATE_BACKEND,
70 | ]
71 |
72 | WSGI_APPLICATION = 'mysite.wsgi.application'
73 |
74 |
75 | # Database
76 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases
77 |
78 | DATABASES = {
79 | 'default': {
80 | 'ENGINE': 'django.db.backends.sqlite3',
81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
82 | }
83 | }
84 |
85 |
86 | # Password validation
87 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
88 |
89 | AUTH_PASSWORD_VALIDATORS = [
90 | {
91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
92 | },
93 | {
94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
95 | },
96 | {
97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
98 | },
99 | {
100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
101 | },
102 | ]
103 |
104 |
105 | # Internationalization
106 | # https://docs.djangoproject.com/en/2.1/topics/i18n/
107 |
108 | LANGUAGE_CODE = 'en-us'
109 |
110 | TIME_ZONE = 'UTC'
111 |
112 | USE_I18N = True
113 |
114 | USE_L10N = True
115 |
116 | USE_TZ = True
117 |
118 |
119 | # Static files (CSS, JavaScript, Images)
120 | # https://docs.djangoproject.com/en/2.1/howto/static-files/
121 |
122 | STATIC_URL = '/static/'
123 |
--------------------------------------------------------------------------------
/assets/sample/urls.py:
--------------------------------------------------------------------------------
1 | """mysite URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path
18 |
19 | from crudlfap import crudlfap
20 |
21 | urlpatterns = [
22 | crudlfap.site.urlpattern,
23 | path('admin/', admin.site.urls),
24 | ]
25 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.contrib.sessions.backends.base import SessionBase
3 | from django.test.client import RequestFactory as drf
4 |
5 |
6 | class RequestFactory(drf):
7 | def __init__(self, user):
8 | self.user = user
9 | super().__init__()
10 |
11 | def generic(self, *args, **kwargs):
12 | request = super().generic(*args, **kwargs)
13 | request.session = SessionBase()
14 | request.user = self.user
15 | return request
16 |
17 |
18 | @pytest.fixture
19 | def srf():
20 | from django.contrib.auth.models import AnonymousUser
21 | return RequestFactory(AnonymousUser())
22 |
--------------------------------------------------------------------------------
/docker-compose.persist.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 | services:
3 | django:
4 | restart: always
5 | volumes:
6 | - ./data/media:/app/media
7 | - ./log:/app/log
8 | labels:
9 | - "io.yourlabs.compose.mkdir=./data/media,./log:1000:1000:0750"
10 | - "traefik.http.middlewares.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-redirect.redirectregex.regex=^https?://${HOST}/(.*)"
11 | - "traefik.http.middlewares.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-redirect.redirectregex.replacement=https://www.${HOST}/$${1}"
12 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.middlewares=${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-redirect"
13 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.entryPoints=websecure"
14 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.rule=host(`${HOST}`, `www.${HOST}`)"
15 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.tls=true"
16 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.tls.certResolver=leresolver"
17 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.service=${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl"
18 | - "traefik.http.services.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}-ssl.loadBalancer.server.port=8000"
19 | - "traefik.docker.network=web"
20 | networks:
21 | - web
22 | - default
23 |
24 | postgres:
25 | restart: always
26 | networks:
27 | - default
28 | volumes:
29 | - ./data/postgres:/var/lib/postgresql/data
30 | - ./dump:/dump
31 | labels:
32 | - "io.yourlabs.compose.mkdir=./dump,./data/postgres,./log/postgres:999:999:0700"
33 |
34 | networks:
35 | web:
36 | external: true
37 |
--------------------------------------------------------------------------------
/docker-compose.traefik.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 | services:
3 | django:
4 | labels:
5 | - "traefik.enable=true"
6 | - "traefik.docker.network=web"
7 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}.entryPoints=web"
8 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}.rule=host(`${HOST}`)"
9 | - "traefik.http.routers.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}.service=${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}"
10 | - "traefik.http.services.${CI_PROJECT_SLUG}-${CI_ENVIRONMENT_SLUG}.loadBalancer.server.port=8000"
11 | networks:
12 | - default
13 | - web
14 |
15 | networks:
16 | web:
17 | external: true
18 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.5'
2 | services:
3 | django:
4 | # DO NOT REMOVE
5 | # Ensures we keep access logs accross re-creates of the container
6 | # Use journalctl CONTAINER_NAME=production_backend_1 to see them
7 | logging:
8 | driver: journald
9 |
10 | build:
11 | dockerfile: Dockerfile
12 | context: .
13 | volumes:
14 | - /app/media
15 | - /app/log
16 | - /tmp
17 | environment:
18 | - HOST
19 | - PROTO
20 | - ADMINS
21 | - BASICAUTH_ENABLE
22 | - CI_ENVIRONMENT_NAME
23 | - CI_COMMIT_SHA
24 | - SENTRY_DSN
25 | - MEDIA_ROOT=/app/media
26 | - LOG_DIR=/app/log
27 | - IPFS_PATH=/app/.ipfs
28 | - EMAIL_HOST
29 | - EMAIL_HOST_USER
30 | - EMAIL_HOST_PASSWORD
31 | - EMAIL_USE_SSL
32 | - EMAIL_USE_TLS
33 | - EMAIL_PORT
34 | - DEFAULT_FROM_EMAIL
35 | - SECRET_KEY
36 | - DB_NAME=django
37 | - DB_USER=django
38 | - DB_PASS=django
39 | - DB_ENGINE=django.db.backends.postgresql
40 | - DB_HOST=postgres
41 |
42 | postgres:
43 | logging:
44 | driver: journald
45 | image: postgres:13
46 | environment:
47 | - POSTGRES_DB=django
48 | - POSTGRES_USER=django
49 | - POSTGRES_PASSWORD=django
50 | volumes:
51 | - /var/lib/postgresql/data
52 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = CRUDLFA
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 |
4 | import django
5 |
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "crudlfap_example.settings")
7 | os.environ.setdefault("DEBUG", "1")
8 |
9 | django.setup()
10 |
11 | # -*- coding: utf-8 -*-
12 | #
13 | # CRUDLFA+ documentation build configuration file, created by
14 | # sphinx-quickstart on Sat Nov 4 15:38:36 2017.
15 | #
16 | # This file is execfile()d with the current directory set to its
17 | # containing dir.
18 | #
19 | # Note that not all possible configuration values are present in this
20 | # autogenerated file.
21 | #
22 | # All configuration values have a default; values that are commented out
23 | # serve to show the default.
24 |
25 | # If extensions (or modules to document with autodoc) are in another directory,
26 | # add these directories to sys.path here. If the directory is relative to the
27 | # documentation root, use os.path.abspath to make it absolute, like shown here.
28 | #
29 | # import os
30 | # import sys
31 | # sys.path.insert(0, os.path.abspath('.'))
32 |
33 |
34 | # -- General configuration ------------------------------------------------
35 |
36 | # If your documentation needs a minimal Sphinx version, state it here.
37 | #
38 | # needs_sphinx = '1.0'
39 |
40 | # Add any Sphinx extension module names here, as strings. They can be
41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
42 | # ones.
43 | extensions = ['sphinx.ext.autodoc',
44 | 'sphinx.ext.viewcode',
45 | 'sphinx.ext.githubpages']
46 |
47 | # Add any paths that contain templates here, relative to this directory.
48 | templates_path = ['_templates']
49 |
50 | # The suffix(es) of source filenames.
51 | # You can specify multiple suffix as a list of string:
52 | #
53 | # source_suffix = ['.rst', '.md']
54 | source_suffix = '.rst'
55 |
56 | # The master toctree document.
57 | master_doc = 'index'
58 |
59 | # General information about the project.
60 | project = 'CRUDLFA+'
61 | copyright = '2017, James Pic & Contributors'
62 | author = 'James Pic & Contributors'
63 |
64 | # The version info for the project you're documenting, acts as replacement for
65 | # |version| and |release|, also used in various other places throughout the
66 | # built documents.
67 | #
68 | # The short X.Y version.
69 | version = '0.0'
70 | # The full version, including alpha/beta/rc tags.
71 | release = '0.0'
72 |
73 | # The language for content autogenerated by Sphinx. Refer to documentation
74 | # for a list of supported languages.
75 | #
76 | # This is also used if you do content translation via gettext catalogs.
77 | # Usually you set "language" from the command line for these cases.
78 | language = None
79 |
80 | # List of patterns, relative to source directory, that match files and
81 | # directories to ignore when looking for source files.
82 | # This patterns also effect to html_static_path and html_extra_path
83 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
84 |
85 | # The name of the Pygments (syntax highlighting) style to use.
86 | pygments_style = 'sphinx'
87 |
88 | # If true, `todo` and `todoList` produce output, else they produce nothing.
89 | todo_include_todos = False
90 |
91 |
92 | # -- Options for HTML output ----------------------------------------------
93 |
94 | # The theme to use for HTML and HTML Help pages. See the documentation for
95 | # a list of builtin themes.
96 | #
97 | # html_theme = 'alabaster'
98 |
99 | # Theme options are theme-specific and customize the look and feel of a theme
100 | # further. For a list of options available for each theme, see the
101 | # documentation.
102 | #
103 | # html_theme_options = {}
104 |
105 | # Add any paths that contain custom static files (such as style sheets) here,
106 | # relative to this directory. They are copied after the builtin static files,
107 | # so a file named "default.css" will overwrite the builtin "default.css".
108 | html_static_path = ['_static']
109 |
110 | # Custom sidebar templates, must be a dictionary that maps document names
111 | # to template names.
112 | #
113 | # This is required for the alabaster theme
114 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
115 | html_sidebars = {
116 | '**': [
117 | 'about.html',
118 | 'navigation.html',
119 | 'relations.html', # needs 'show_related': True theme option to display
120 | 'searchbox.html',
121 | 'donate.html',
122 | ]
123 | }
124 |
125 |
126 | # -- Options for HTMLHelp output ------------------------------------------
127 |
128 | # Output file base name for HTML help builder.
129 | htmlhelp_basename = 'CRUDLFAdoc'
130 |
131 |
132 | # -- Options for LaTeX output ---------------------------------------------
133 |
134 | latex_elements = {
135 | # The paper size ('letterpaper' or 'a4paper').
136 | #
137 | # 'papersize': 'letterpaper',
138 |
139 | # The font size ('10pt', '11pt' or '12pt').
140 | #
141 | # 'pointsize': '10pt',
142 |
143 | # Additional stuff for the LaTeX preamble.
144 | #
145 | # 'preamble': '',
146 |
147 | # Latex figure (float) alignment
148 | #
149 | # 'figure_align': 'htbp',
150 | }
151 |
152 | # Grouping the document tree into LaTeX files. List of tuples
153 | # (source start file, target name, title,
154 | # author, documentclass [howto, manual, or own class]).
155 | latex_documents = [
156 | (master_doc, 'CRUDLFA.tex', 'CRUDLFA+ Documentation',
157 | 'James Pic \\& Contributors', 'manual'),
158 | ]
159 |
160 |
161 | # -- Options for manual page output ---------------------------------------
162 |
163 | # One entry per manual page. List of tuples
164 | # (source start file, name, description, authors, manual section).
165 | man_pages = [
166 | (master_doc, 'crudlfa', 'CRUDLFA+ Documentation',
167 | [author], 1)
168 | ]
169 |
170 |
171 | # -- Options for Texinfo output -------------------------------------------
172 |
173 | # Grouping the document tree into Texinfo files. List of tuples
174 | # (source start file, target name, title, author,
175 | # dir menu entry, description, category)
176 | texinfo_documents = [
177 | (master_doc, 'CRUDLFA', 'CRUDLFA+ Documentation',
178 | author, 'CRUDLFA', 'One line description of project.',
179 | 'Miscellaneous'),
180 | ]
181 |
182 |
183 |
184 | # -- Options for Epub output ----------------------------------------------
185 |
186 | # Bibliographic Dublin Core info.
187 | epub_title = project
188 | epub_author = author
189 | epub_publisher = author
190 | epub_copyright = copyright
191 |
192 | # The unique identifier of the text. This can be a ISBN number
193 | # or the project homepage.
194 | #
195 | # epub_identifier = ''
196 |
197 | # A unique identification for the text.
198 | #
199 | # epub_uid = ''
200 |
201 | # A list of files that should not be packed into the epub file.
202 | epub_exclude_files = ['search.html']
203 |
204 |
205 |
--------------------------------------------------------------------------------
/docs/crudlfap_auth/index.rst:
--------------------------------------------------------------------------------
1 | crudlfap_auth: crudlfap module for django.contrib.auth
2 | ======================================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 | :caption: Contents:
7 |
8 | views
9 |
--------------------------------------------------------------------------------
/docs/crudlfap_auth/views.rst:
--------------------------------------------------------------------------------
1 | Auth Views
2 | ~~~~~~~~~~
3 |
4 | Source is located in the :py:class:`~crudlfap_auth.views`, which
5 | we'll describe here.
6 |
7 | .. automodule:: crudlfap_auth.views
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/factory.rst:
--------------------------------------------------------------------------------
1 | Factory DRY patterns
2 | ~~~~~~~~~~~~~~~~~~~~
3 |
4 | .. automodule:: crudlfap.factory
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. CRUDLFA+ documentation master file, created by
2 | sphinx-quickstart on Sat Nov 4 15:38:36 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to CRUDLFA+'s documentation!
7 | ====================================
8 |
9 | CRUDLFA+ stands for Create Read Update Delete List Form Autocomplete and more.
10 |
11 | This plugin for Django makes a rich user interface from Django models.
12 |
13 | .. toctree::
14 | :maxdepth: 2
15 | :caption: Contents:
16 |
17 | install
18 | tutorial
19 | route
20 | router
21 | settings
22 | views
23 | factory
24 | crudlfap_auth/index
25 |
26 | Indices and tables
27 | ==================
28 |
29 | * :ref:`genindex`
30 | * :ref:`modindex`
31 | * :ref:`search`
32 |
--------------------------------------------------------------------------------
/docs/install.rst:
--------------------------------------------------------------------------------
1 | Install CRUDLFA+ module
2 | ~~~~~~~~~~~~~~~~~~~~~~~
3 |
4 | This section concerns
5 | This package can be installed from PyPi by running:
6 |
7 | Installing from PyPi
8 | --------------------
9 |
10 | If you are just getting started with CRUDLFA+, it is recommended that you
11 | start by installing the latest version from the Python Package Index (PyPi_).
12 | To install CRUDLFA+ from PyPi using pip run the following command in your terminal.
13 |
14 | .. code-block:: bash
15 |
16 | pip install crudlfap
17 |
18 | If you are not in a virtualenv_, the above will fail if not executed as root,
19 | in this case use ``install --user``::
20 |
21 | pip install --user crudlfap
22 |
23 | With development packages
24 | -------------------------
25 |
26 | If you intend to run the ``crudlfap dev`` command, then you should have the
27 | development dependencies by adding ``[dev]``::
28 |
29 | pip install (--user) crudlfap[dev]
30 |
31 | Then, you should see the example project running on port 8000 with command::
32 |
33 | crudlfap dev
34 |
35 | Installing from GitHub
36 | ----------------------
37 |
38 | You can install the latest current trunk of crudlfap directly from GitHub using pip_.
39 |
40 | .. code-block:: bash
41 |
42 | pip install --user -e git+git://github.com/yourlabs/crudlfap.git@master#egg=crudlfap[dev]
43 |
44 | .. warning:: ``[dev]``, ``--user``, ``@master`` are all optionnal above.
45 |
46 | Installing from source
47 | ----------------------
48 |
49 | 1. Download a copy of the code from GitHub. You may need to install git_.
50 |
51 | .. code-block:: bash
52 |
53 | git clone https://github.com/yourlabs/crudlfap.git
54 |
55 | 2. Install the code you have just downloaded using pip, assuming your current
56 | working directory has not changed since the previous command it could be::
57 |
58 | pip install -e ./crudlfap[dev]
59 |
60 | Move on to the :doc:`tutorial`.
61 |
62 | .. _git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
63 | .. _pip: https://pip.pypa.io/en/stable/installing/
64 | .. _PyPi: https://pypi.python.org/pypi
65 | .. _virtualenv: https://virtualenv.pypa.io/
66 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | sphinx-js
3 | django
4 | # https://github.com/pallets/jinja/issues/1585
5 | MarkupSafe==2.0.1
6 | .[project]
7 |
--------------------------------------------------------------------------------
/docs/route.rst:
--------------------------------------------------------------------------------
1 | Route class
2 | ~~~~~~~~~~~
3 |
4 | .. automodule:: crudlfap.route
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/router.rst:
--------------------------------------------------------------------------------
1 | URLPatterns autogeneration mechanisms: Router
2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 |
4 | One of the key architectural concepts of CRUDLFA+ is the ability to tie a group
5 | of view with a model class to autogenerate urlpatterns. This chapter reviews
6 | the different mechanisms in place and how they are overridable.
7 |
8 | Source is located in the :py:class:`~crudlfap.router.Router`, which
9 | we'll describe here.
10 |
11 | .. automodule:: crudlfap.router
12 | :members:
13 |
--------------------------------------------------------------------------------
/docs/screenshots/add-modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/docs/screenshots/add-modal.png
--------------------------------------------------------------------------------
/docs/screenshots/detail-template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/docs/screenshots/detail-template.png
--------------------------------------------------------------------------------
/docs/screenshots/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/docs/screenshots/list.png
--------------------------------------------------------------------------------
/docs/screenshots/update-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/docs/screenshots/update-form.png
--------------------------------------------------------------------------------
/docs/settings.rst:
--------------------------------------------------------------------------------
1 | Settings
2 | ~~~~~~~~
3 |
4 | Project
5 | =======
6 |
7 | .. automodule:: crudlfap.settings
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/tutorial.rst:
--------------------------------------------------------------------------------
1 | CRUDLFA+ Tutorial
2 | ~~~~~~~~~~~~~~~~~
3 |
4 | About document
5 | ==============
6 |
7 | This document attempts to teach the patterns you can use, and at the same time
8 | go through every feature. The document strives to teach CRUDLFA+ as efficiently
9 | as possible. If it becomes too long, we will see how we refactor the document,
10 | until then, it serves as main documentation. Please contribute any modification
11 | you feel this document needs to fit its purpose.
12 |
13 | About module
14 | ============
15 |
16 | CRUDLFA+ strives to provide a modern UI for Django generic views out of the
17 | box, but all defaults should also be overiddable as conveniently as possible.
18 | It turns out that Django performs extremely well already, and pushing Django's
19 | philosophy such as DRY as far as possible works very well for me.
20 |
21 | Start a CRUDLFA+ project
22 | ========================
23 |
24 | Make sure you install all project dependencies::
25 |
26 | pip install crudlfap[project]
27 |
28 | And create a Django project::
29 |
30 | django-admin startproject yourproject
31 |
32 | Copy this ``settings.py`` which provides working settings for CRUDLFA+
33 | and allows to control settings with environment variables:
34 |
35 | .. literalinclude:: ../src/crudlfap_example/settings.py
36 |
37 | And this ``urls.py``:
38 |
39 | .. literalinclude:: ../src/crudlfap_example/urls.py
40 |
41 | You may also install manually, but the procedure might change over time.
42 |
43 | Create a ``crudlfap.py``
44 | ========================
45 |
46 | You need to create a Django app like with ``./manage.py startapp yourapp``. It
47 | creates a yourapp directory and you need to add yourapp to
48 | ``settings.INSTALLED_APPS``.
49 |
50 | Then, you can start a ``yourapp/crudlfap.py`` file, where you can define model
51 | CRUDs, hook into the rendering of CRUDLFA+ (ie. menus) and so on.
52 |
53 | Define a Router
54 | ===============
55 |
56 | Register a CRUD with default views using Router.register()
57 | ----------------------------------------------------------
58 |
59 | Just add a ``crudlfap.py`` file in one of your installed apps, and the
60 | :py:class:`~crudlfap.apps.DefaultConfig` will autodiscover them, this example
61 | shows how to enable the default CRUD for a custom model:
62 |
63 | .. literalinclude:: ../src/crudlfap_example/artist/crudlfap.py
64 |
65 | In this case, the :py:class:`~crudlfap.router.Router` will get the views
66 | it should serve from the :py:data:`~crudlfap.settings.CRUDLFAP_VIEWS`
67 | setting.
68 |
69 | Custom view parameters with View.clone()
70 | ----------------------------------------
71 |
72 | If you want to specify views in the router:
73 |
74 | .. literalinclude:: ../src/crudlfap_example/song/crudlfap.py
75 |
76 | Using the :py:meth:`~crudlfap.factory.Factory.clone()` classmethod will
77 | define a subclass on the fly with the given attributes.
78 |
79 | URLs
80 | ====
81 |
82 | The easiest configuration is to generate patterns from the default registry::
83 |
84 | from crudlfap import shortcuts as crudlfap
85 |
86 | urlpatterns = [
87 | crudlfap.site.urlpattern
88 | ]
89 |
90 | Or, to sit in ``/admin``::
91 |
92 | crudlfap.site.urlpath = 'admin'
93 |
94 | urlpatterns = [
95 | crudlfap.site.urlpattern,
96 | # your patterns ..
97 | ]
98 |
99 | Changing home page
100 | ==================
101 |
102 | CRUDLFA+ so far relies on Jinja2 and provides a configuration
103 | where it finds templates in app_dir/jinja2.
104 |
105 | As such, a way to override the home page template is to create a directory
106 | "jinja2" in one of your apps - personnaly i add the project itself to
107 | INSTALLED_APPS, sorry if you have hard feelings about it but i love to do
108 | that, have a place to put project-specific stuff in general - and in the
109 | `jinja2` directory create a `crudlfap/home.html` file.
110 |
111 | You will also probably want to override `crudlfap/base.html`. But where it
112 | gets more interresting is when you replace the home view with your own.
113 | Example, still in urls.py::
114 |
115 | from crudlfap import shortcuts as crudlfap
116 | from .views import Dashboard # your view
117 |
118 | crudlfap.site.title = 'Your Title' # used by base.html
119 | crudlfap.site.urlpath = 'admin' # example url prefix
120 | crudlfap.site.views['home'] = views.Dashboard
121 |
122 | urlpatterns = [
123 | crudlfap.site.get_urlpattern(),
124 | ]
125 |
126 | So, there'd be other ways to acheive this but that's how i like to
127 | do it.
128 |
129 | Add menu item
130 | =============
131 |
132 | You can hook into CRUDLFA+ menu rendering, ie.:
133 |
134 | .. literalinclude:: ../src/crudlfap_registration/crudlfap.py
135 |
136 | Create route from view
137 | ======================
138 |
139 | The following example returns a :py:class:`~crudlfap.route.Route` as needed by
140 | :py:class:`~crudlfap.router.Router` and
141 | :py:class:`~crudlfap.registry.Registry`:
142 |
143 | .. code-block:: python
144 |
145 | Route.factory(
146 | LoginView,
147 | title=_('Login'),
148 | title_submit=_('Login'),
149 | title_menu=_('Login'),
150 | menus=['main'],
151 | redirect_authenticated_user=True,
152 | authenticate=False,
153 | icon='login',
154 | has_perm=lambda self: not self.request.user.is_authenticated,
155 | )
156 |
157 | Useful to add external apps views to routers or site.views, prior to the menu
158 | hook feature.
159 |
160 | Create list actions
161 | ===================
162 |
163 | List actions, such as the delete action, can be implemented as such::
164 |
165 | class DeployMixin(crudlfap.ActionMixin):
166 | style = ''
167 | icon = 'send'
168 | success_url_next = True
169 | color = 'green'
170 | form_class = forms.Form
171 | permission_shortcode = 'send'
172 | label = 'deploy'
173 |
174 | def get_success_url(self):
175 | return self.router['list'].reverse()
176 |
177 | def has_perm_object(self):
178 | return self.object.state == 'held'
179 |
180 |
181 | class TransactionDeployView(DeployMixin, crudlfap.ObjectFormView):
182 | def form_valid(self):
183 | self.object.state = 'deploy'
184 | self.object.save()
185 | return super().form_valid()
186 |
187 |
188 | class TransactionDeployMultipleView(DeployMixin, crudlfap.ObjectsFormView):
189 | def form_valid(self):
190 | self.object_list.filter(state='held').update(state='deploy')
191 | return super().form_valid()
192 |
193 | API
194 | ===
195 |
196 | CRUDLFA+ now offers a REST API interface which is documented with swagger on
197 | ``/api``.
198 |
199 | Usage
200 | -----
201 |
202 | It requires to set the ``Content-Type: application/json`` header. For example:
203 |
204 | .. code-block:: python
205 |
206 | import requests
207 |
208 | response = requests.get(
209 | f'http://localhost:8000/blockchain',
210 | headers={'Content-Type': 'application/json'}
211 | )
212 | assert response.status_code == 200
213 | blockchains = response.json()
214 |
215 | POST requests require a CSRF token that you may obtain as a cookie with any GET
216 | request, but it is advised that you keep it up to date by using a request
217 | session.
218 |
219 | .. code-block:: python
220 |
221 | import requests
222 | session = requests.session()
223 | session.get('http://localhost:8000')
224 | response = session.post(
225 | f'{site}/auth/login/',
226 | dict(
227 | username='user', password='user'
228 | ),
229 | headers={'X-CSRFToken': session.cookies['csrftoken']},
230 | allow_redirects=False,
231 | )
232 | assert response.status_code == 302
233 |
234 |
235 | Then you can make more POST requests ie. to create an object:
236 |
237 | .. code-block:: python
238 |
239 | response = session.post(
240 | f'{site}/account/create',
241 | json=dict(
242 | blockchain=123,
243 | ),
244 | headers={
245 | 'Content-Type': 'application/json',
246 | 'X-CSRFToken': session.cookies['csrftoken'],
247 | }
248 | )
249 | assert response.status_code == 201
250 |
251 | In case of error, it will return a 4xx response with the error details in a
252 | JSON response.
253 |
254 | Customize the API
255 | -----------------
256 |
257 | By default, views will call the router's
258 | :py:meth:`~crudlfap.router.Router.serialize(obj, fields)` method. You may override it,
259 | or just configure :py:attr:`~crudlfap.router.Router.json_fields` attribute. To
260 | serialize each field, it will try to find ``get_FIELDNAME_json()`` method,
261 | otherwise use the default
262 | :py:meth:`~crudlfap.router.Router.get_FIELDNAME_json(obj, field)` method which
263 | you might has well override. It will use the related router to serialize any
264 | related object.
265 |
266 | Then, you may override both of these at the view level if you want.
267 |
--------------------------------------------------------------------------------
/docs/views.rst:
--------------------------------------------------------------------------------
1 | Views
2 | ~~~~~
3 |
4 | Source is located in the :py:class:`~crudlfap.views.generic`, which
5 | we'll describe here.
6 |
7 | .. automodule:: crudlfap.views.generic
8 | :members:
9 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crudlfap_example.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/nightwatch/eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 8,
4 | "sourceType": "module",
5 | "ecmaFeatures": {
6 | "jsx": true,
7 | "browser": true,
8 | "es6": true,
9 | "jest": true
10 | }
11 | },
12 | "globals": {
13 | "module": true,
14 | "exports": true,
15 | "process": true
16 | },
17 | "rules": {
18 | "indent": [
19 | "error",
20 | 2
21 | ],
22 | "linebreak-style": [
23 | "error",
24 | "unix"
25 | ],
26 | "quotes": [
27 | "error",
28 | "single"
29 | ],
30 | "semi": [
31 | "error",
32 | "always"
33 | ],
34 | "comma-dangle": [
35 | "error",
36 | {
37 | "arrays": "never",
38 | "objects": "never",
39 | "imports": "never",
40 | "exports": "never",
41 | "functions": "ignore"
42 | }
43 | ],
44 | "object-curly-newline": [
45 | "error",
46 | {
47 | "ObjectExpression": "always",
48 | "ObjectPattern": {
49 | "multiline": true
50 | }
51 | }
52 | ],
53 | "space-before-function-paren": [
54 | "error",
55 | "always"
56 | ],
57 | "no-multiple-empty-lines": [
58 | "error",
59 | {
60 | "max": 1,
61 | "maxEOF": 1
62 | }
63 | ],
64 | "no-trailing-spaces": "error",
65 | "eol-last": [
66 | "error",
67 | "always"
68 | ],
69 | "no-cond-assign": [
70 | "error",
71 | "always"
72 | ],
73 | "key-spacing": [
74 | "error",
75 | {
76 | "beforeColon": false
77 | }
78 | ],
79 | "newline-before-return": "error",
80 | "no-console": "off"
81 | }
82 | }
--------------------------------------------------------------------------------
/nightwatch/globals-driver.js:
--------------------------------------------------------------------------------
1 | const chromedriver = require('chromedriver');
2 | module.exports = {
3 | before: (done) => {
4 | chromedriver.start();
5 |
6 | done();
7 | },
8 |
9 | after: (done) => {
10 | chromedriver.stop();
11 |
12 | done();
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/nightwatch/globals.js:
--------------------------------------------------------------------------------
1 | /* globals process */
2 | const sauceConnectLauncher = require('sauce-connect-launcher');
3 |
4 | let sauceConnectTunnel;
5 |
6 | module.exports = {
7 | before: (done) => {
8 | sauceConnectLauncher({
9 | username: process.env.SAUCELABS_USERNAME,
10 | accessKey: process.env.SAUCELABS_TOKEN
11 | }, function (err, sauceConnectProcess) {
12 | if (err) {
13 | console.error('Sauce Connect Error : ', err.message);
14 | process.exit(1);
15 | }
16 | console.log('Sauce Connect ready');
17 | sauceConnectTunnel = sauceConnectProcess;
18 | done();
19 | });
20 | },
21 | after: (done) => {
22 | sauceConnectTunnel.close(function () {
23 | console.log('Closed Sauce Connect process');
24 | done();
25 | });
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/nightwatch/nightwatch-driver.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_folders": [
3 | "dist/tests"
4 | ],
5 | "output_folder": "reports",
6 | "custom_commands_path": "",
7 | "custom_assertions_path": "",
8 | "page_objects_path": "",
9 | "globals_path": "./globals-driver.js",
10 | "selenium": {
11 | "start_process": false
12 | },
13 | "test_settings": {
14 | "default": {
15 | "selenium_port": 9515,
16 | "selenium_host": "localhost",
17 | "default_path_prefix": "",
18 | "desiredCapabilities": {
19 | "browserName": "chrome",
20 | "chromeOptions": {
21 | "args": [
22 | "--no-sandbox"
23 | ]
24 | },
25 | "acceptSslCerts": true
26 | }
27 | },
28 | "chrome": {
29 | "desiredCapabilities": {
30 | "browserName": "chrome"
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/nightwatch/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | const config = { // we use a nightwatch.conf.js file so we can include comments and helper functions
2 | 'src_folders': [
3 | 'dist/tests'
4 | ],
5 | 'output_folder': 'reports',
6 | 'custom_commands_path': '',
7 | 'custom_assertions_path': '',
8 | 'page_objects_path': '',
9 | 'globals_path': './globals.js',
10 | 'selenium': {
11 | 'start_process': false,
12 | 'server_path': '',
13 | 'log_path': '',
14 | 'host': '127.0.0.1',
15 | 'port': 4444
16 | },
17 | 'test_workers': {
18 | 'enabled': true, 'workers': 'auto'
19 | }, // perform tests in parallel where possible
20 | 'test_settings': {
21 | 'default': {
22 | 'launch_url': 'http://ondemand.saucelabs.com:80',
23 | 'selenium_port': 80,
24 | 'selenium_host': 'ondemand.saucelabs.com',
25 | 'silent': true,
26 | 'screenshots': {
27 | 'enabled': false,
28 | 'path': ''
29 | },
30 | 'username': process.env.SAUCELABS_USERNAME,
31 | 'access_key': process.env.SAUCELABS_TOKEN,
32 | 'skip_testcases_on_fail': false,
33 | 'desiredCapabilities': {
34 | 'browserName': 'chrome',
35 | 'version': 'latest',
36 | 'chromeOptions': {
37 | 'args': [
38 | '--no-sandbox'
39 | ]
40 | },
41 | 'acceptSslCerts': true
42 | }
43 | },
44 | 'chrome': {
45 | 'desiredCapabilities': {
46 | 'browserName': 'chrome',
47 | 'version': 'latest',
48 | 'javascriptEnabled': true,
49 | 'acceptSslCerts': true
50 | }
51 | },
52 | 'internet_explorer': {
53 | 'desiredCapabilities': {
54 | 'browserName': 'internet explorer',
55 | 'version': 'latest',
56 | 'javascriptEnabled': true,
57 | 'acceptSslCerts': true
58 | }
59 | },
60 | 'firefox': {
61 | 'desiredCapabilities': {
62 | 'browserName': 'firefox',
63 | 'version': 'latest',
64 | 'javascriptEnabled': true,
65 | 'acceptSslCerts': true
66 | }
67 | }
68 | }
69 | };
70 | module.exports = config;
71 |
--------------------------------------------------------------------------------
/nightwatch/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nightwatch-typescript-starter",
3 | "version": "1.0.0",
4 | "description": "Simple nightwatch.js Typescript starter",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "./node_modules/.bin/tsc",
8 | "pretest": "yarn install && yarn run build",
9 | "lint": "eslint -c ./eslintrc.json .",
10 | "test": "./node_modules/.bin/nightwatch"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/anjmao/nightwatch-typescript-starter.git"
15 | },
16 | "author": "Andzej Maciusovic",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/anjmao/nightwatch-typescript-starter/issues"
20 | },
21 | "homepage": "https://github.com/anjmao/nightwatch-typescript-starter#readme",
22 | "devDependencies": {
23 | "@types/nightwatch": "^0.9.8",
24 | "@types/node": "^8.10.10",
25 | "chromedriver": "^2.33.2",
26 | "nightwatch": "^0.9.21",
27 | "typescript": "^2.8.3",
28 | "eslint": "^4.19.1",
29 | "sauce-connect-launcher": "^1.2.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/nightwatch/shared/CONSTANTS.ts:
--------------------------------------------------------------------------------
1 | export const CONSTANTS = {
2 | BASE_URL: 'http://localhost:8000',
3 | LOGIN_URL: 'http://localhost:8000/login',
4 | LOGOUT_URL: 'http://localhost:8000/logout',
5 | GROUP: {
6 | BASE_URL: 'http://localhost:8000/group',
7 | CREATE: 'http://localhost:8000/group/create',
8 | INPUT: 'Test Group for sample',
9 | INPUT2: 'Test Group for sample with permission',
10 | EDIT_INPUT: 'Updated Group',
11 | },
12 | ARTIST: {
13 | BASE_URL: 'http://localhost:8000/artist',
14 | CREATE: 'http://localhost:8000/artist/create',
15 | INPUT: 'Test artist for sample',
16 | INPUT2: 'Test artist for sample with permission',
17 | EDIT_INPUT: 'Updated artist',
18 | },
19 | SONGS: {
20 | BASE_URL: 'http://localhost:8000/song',
21 | CREATE: 'http://localhost:8000/song/create',
22 | INPUT: 'Test song for sample',
23 | INPUT2: 'Test song for sample 2',
24 | INPUT_DURATION: 180,
25 | EDIT_INPUT: 'Updated Song',
26 | },
27 | SONGRATING: {
28 | BASE_URL: 'http://localhost:8000/songrating',
29 | CREATE: 'http://localhost:8000/songrating/create',
30 | INPUT: '2',
31 | INPUT2: '3',
32 | EDIT_INPUT: '5',
33 | },
34 | USER: {
35 | BASE_URL: 'http://localhost:8000/user',
36 | CREATE: 'http://localhost:8000/user/create',
37 | INVALID_USER: 'test @123',
38 | EXISTING_USER: 'dev',
39 | INPUT_USER: 'test@123',
40 | INPUT_EMAIL: 'test@123.com',
41 | INPUT2: 'Test User for sample with groups',
42 | EDIT_INPUT: 'Updated@123',
43 | EDIT_EMAIL: 'updated@123.com',
44 |
45 | },
46 | POST: {
47 | BASE_URL: 'http://localhost:8000/post',
48 | CREATE: 'http://localhost:8000/post/create',
49 | INPUT_TITLE: 'Test Post',
50 | INPUT_TITLE2: 'Test Post 2',
51 | EDIT_INPUT: 'Update post'
52 | },
53 | WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT: 15000,
54 | PAUSE_TIMEOUT: 10000,
55 | USER_CREDENTIALS: {
56 | RIGHT: {
57 | USERNAME: 'dev',
58 | PASSWORD: 'dev'
59 | },
60 | WRONG: {
61 | USERNAME: 'dev1',
62 | PASSWORD: 'dev1'
63 | }
64 | },
65 | }
66 |
--------------------------------------------------------------------------------
/nightwatch/tests/artist/validation.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../../shared/CONSTANTS';
3 | import { CommonFunction } from '../../shared/commonFunction';
4 |
5 | module.exports = {
6 | 'Artist : create artist validation': async (browser: NightwatchBrowser) => {
7 | await CommonFunction.loginByDev(browser);
8 | browser
9 | // after login go to song create page direct
10 | .url(CONSTANTS.ARTIST.CREATE)
11 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
12 |
13 | // name input
14 | .assert.containsText('#id_name_container > label', 'Name')
15 | .assert.visible('input[id=id_name]')
16 |
17 | .assert.visible('#form-object-artist > div.modal-footer > button[type=submit]')
18 | .click('#form-object-artist > div.modal-footer > button[type=submit]')
19 |
20 | .pause(CONSTANTS.PAUSE_TIMEOUT)
21 |
22 | .assert.visible('#id_name_container > div > small')
23 | .assert.containsText('#id_name_container > div > small', 'This field is required.')
24 |
25 | .end();
26 | }
27 | };
--------------------------------------------------------------------------------
/nightwatch/tests/group/validation.ts:
--------------------------------------------------------------------------------
1 |
2 | import { NightwatchBrowser } from 'nightwatch';
3 | import { CONSTANTS } from '../../shared/CONSTANTS';
4 | import { CommonFunction } from '../../shared/commonFunction';
5 |
6 | module.exports = {
7 | 'Group : create group validation': async (browser: NightwatchBrowser) => {
8 | await CommonFunction.loginByDev(browser);
9 | browser
10 | // after login go to group create page direct
11 | .url(CONSTANTS.GROUP.CREATE)
12 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
13 | // name input
14 | .assert.containsText('#id_name_container > label', 'Name')
15 | .assert.visible('input[id=id_name]')
16 | // submit button
17 | .assert.visible('#form-object-group .modal-footer button[type=submit]')
18 | .click('#form-object-group .modal-footer button[type=submit]')
19 | .pause(CONSTANTS.PAUSE_TIMEOUT)
20 |
21 | .assert.visible('#id_name_container > div > small')
22 | .assert.containsText('#id_name_container > div > small', 'This field is required.')
23 |
24 | .end();
25 | }
26 | };
--------------------------------------------------------------------------------
/nightwatch/tests/login.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../shared/CONSTANTS';
3 |
4 | module.exports = {
5 | 'Login : submit with wrong credentials': (browser: NightwatchBrowser) => {
6 | browser
7 | .url(CONSTANTS.LOGIN_URL)
8 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
9 |
10 | .assert.visible('input[id=id_username]')
11 | .setValue('input[id=id_username]', CONSTANTS.USER_CREDENTIALS.WRONG.USERNAME)
12 |
13 | .assert.visible('input[id=id_password]')
14 | .setValue('input[id=id_password]', CONSTANTS.USER_CREDENTIALS.WRONG.PASSWORD)
15 |
16 | .assert.visible('button[type=submit]')
17 | .click('button[type=submit]')
18 | .pause(CONSTANTS.PAUSE_TIMEOUT)
19 |
20 | // after login
21 | .assert.containsText('small[class=error]', 'Please enter a correct username and password. Note that both fields may be case-sensitive.')
22 | .end();
23 | },
24 |
25 | 'Login : submit with correct credentials': (browser: NightwatchBrowser) => {
26 | browser
27 | .url(CONSTANTS.LOGIN_URL)
28 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
29 |
30 | .assert.visible('input[id=id_username]')
31 | .setValue('input[id=id_username]', CONSTANTS.USER_CREDENTIALS.RIGHT.USERNAME)
32 |
33 | .assert.visible('input[id=id_password]')
34 | .setValue('input[id=id_password]', CONSTANTS.USER_CREDENTIALS.RIGHT.PASSWORD)
35 |
36 | .assert.visible('button[type=submit]')
37 | .click('button[type=submit]')
38 |
39 | // after login
40 | .waitForElementVisible('.container .orange-text', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
41 | // .assert.title('Home - CRUDLFA+')
42 | .assert.visible('a[class=waves-effect]')
43 | .end();
44 | }
45 | };
--------------------------------------------------------------------------------
/nightwatch/tests/logout.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../shared/CONSTANTS';
3 |
4 | module.exports = {
5 | before: function (browser) {
6 | // console.log("Before working!");
7 | // login user with correct crednetials
8 | browser
9 | .url(CONSTANTS.LOGIN_URL)
10 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
11 |
12 | .assert.visible('input[id=id_username]')
13 | .setValue('input[id=id_username]', CONSTANTS.USER_CREDENTIALS.RIGHT.USERNAME)
14 |
15 | .assert.visible('input[id=id_password]')
16 | .setValue('input[id=id_password]', CONSTANTS.USER_CREDENTIALS.RIGHT.PASSWORD)
17 |
18 | .assert.visible('button[type=submit]')
19 | .click('button[type=submit]')
20 |
21 | .waitForElementVisible('ul[class="right"] li a[href="/logout"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
22 | },
23 | 'Logout': (browser: NightwatchBrowser) => {
24 | browser
25 | // after login
26 | .click('a[href="/logout"]')
27 |
28 | // on logout page
29 | .waitForElementVisible('ul[class="right"] li a[href="/login"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
30 |
31 | .assert.urlEquals(CONSTANTS.LOGOUT_URL, 'Logout url is the correct')
32 |
33 | .end();
34 | }
35 |
36 | };
--------------------------------------------------------------------------------
/nightwatch/tests/menu.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from './../shared/CONSTANTS';
3 | module.exports = {
4 | 'Before Login: Menu should visible when click on the menu icon': (browser: NightwatchBrowser) => {
5 | browser
6 | .url(CONSTANTS.LOGIN_URL)
7 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
8 |
9 | .assert.visible('span[class="sidenav-trigger"]')
10 | .element('id', 'slide-out', function (result) {
11 | browser.assert.equal(result.status, false)
12 | })
13 | .click('span[class="sidenav-trigger"]')
14 | .waitForElementVisible('ul[id="slide-out"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
15 | .isVisible('ul[id="slide-out"]', (result) => {
16 | browser.assert.equal(result.value, true)
17 | })
18 | .click('ul#slide-out li.no-padding a[href="/"]')
19 | .end();
20 | },
21 |
22 | 'Before Login: Menu should contains "login" option': (browser: NightwatchBrowser) => {
23 | browser
24 | .url(CONSTANTS.LOGIN_URL)
25 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
26 | .waitForElementVisible('span[class="sidenav-trigger"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
27 |
28 | .assert.visible('span[class="sidenav-trigger"]')
29 | .click('span[class="sidenav-trigger"]')
30 | .waitForElementVisible('ul#slide-out li.no-padding a[href="/login"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
31 |
32 | .elements('css selector', 'ul#slide-out li.no-padding a[href="/login"]', (result) => {
33 | browser.assert.equal(result.value.length, 1)
34 | })
35 | .end();
36 | },
37 |
38 | 'After Login: Menu should contains the "logout" option': (browser: NightwatchBrowser) => {
39 | browser
40 | .url(CONSTANTS.LOGIN_URL)
41 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
42 |
43 | .elements('css selector', 'ul#slide-out li.no-padding a[href="/login"]', (result) => {
44 | browser.assert.equal(result.value.length, 1)
45 | })
46 |
47 | .click('ul#slide-out li.no-padding a[href="/login"]')
48 |
49 | .waitForElementVisible('input[id=id_username]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
50 | .assert.visible('input[id=id_username]')
51 | .setValue('input[id=id_username]', CONSTANTS.USER_CREDENTIALS.RIGHT.USERNAME)
52 | .assert.visible('input[id=id_password]')
53 | .setValue('input[id=id_password]', CONSTANTS.USER_CREDENTIALS.RIGHT.PASSWORD)
54 |
55 | .assert.visible('button[type=submit]')
56 | .click('button[type=submit]')
57 | .waitForElementVisible('ul[class="right"] li a[href="/logout"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
58 |
59 | .assert.visible('span[class="sidenav-trigger"]')
60 | .click('span[class="sidenav-trigger"]')
61 | .waitForElementVisible('ul[id="slide-out"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
62 | .assert.visible('ul[id="slide-out"]')
63 |
64 | .elements('css selector', '#slide-out > li:nth-child(4) > a[href="/logout"]', (result) => {
65 | browser.assert.equal(result.value.length, 1)
66 | })
67 | .end();
68 | },
69 | 'After Logout: Menu should contains "login" option': (browser: NightwatchBrowser) => {
70 | browser
71 | .url(CONSTANTS.BASE_URL)
72 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
73 | .assert.visible('span[class="sidenav-trigger"]')
74 | .element('id', 'slide-out', function (result) {
75 | browser.assert.equal(result.status, false)
76 | })
77 | .click('span[class="sidenav-trigger"]')
78 | .waitForElementVisible('ul[class="right"] li a[href="/login"]', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
79 | .isVisible('ul[id="slide-out"]', (result) => {
80 | browser.assert.equal(result.value, true)
81 | })
82 |
83 | .elements('css selector', 'ul#slide-out li.no-padding a[href="/login"]', (result) => {
84 | browser.assert.equal(result.value.length, 1)
85 | })
86 | .end();
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/nightwatch/tests/post/validation.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../../shared/CONSTANTS';
3 | import { CommonFunction } from '../../shared/commonFunction';
4 |
5 | module.exports = {
6 | 'Post : create post validation': async (browser: NightwatchBrowser) => {
7 | await CommonFunction.loginByDev(browser);
8 | browser
9 | // after login go to post create page direct
10 | .url(CONSTANTS.POST.CREATE)
11 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
12 | // name input
13 | .assert.containsText('#id_name_container > label', 'Title')
14 | .assert.visible('input[id=id_name]')
15 | // publish input
16 | .assert.containsText('#id_publish_container > label', 'Publish')
17 | .assert.visible('input[id=id_publish]')
18 | .clearValue('input[id=id_publish]')
19 | .pause(CONSTANTS.PAUSE_TIMEOUT)
20 |
21 | // owner selection
22 | .assert.containsText('#id_owner_container > label', 'Owner')
23 | .assert.visible('#id_owner_container > div.select-wrapper > input')
24 |
25 | // submit button
26 | .assert.visible('#form-object-post .modal-footer button[type=submit]')
27 | .click('#form-object-post .modal-footer button[type=submit]')
28 | .pause(CONSTANTS.PAUSE_TIMEOUT)
29 |
30 | .assert.visible('#id_name_container > div > small')
31 | .assert.containsText('#id_name_container > div > small', 'This field is required.')
32 |
33 | .assert.visible('#id_publish_container > div > small')
34 | .assert.containsText('#id_publish_container > div > small', 'This field is required.')
35 |
36 | .assert.visible('#id_owner_container > div > small')
37 | .assert.containsText('#id_owner_container > div > small', 'This field is required.')
38 |
39 | .end();
40 | }
41 | };
--------------------------------------------------------------------------------
/nightwatch/tests/songRating/validation.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../../shared/CONSTANTS';
3 | import { CommonFunction } from '../../shared/commonFunction';
4 |
5 | module.exports = {
6 | 'SongRating : create song-rating validation': async (browser: NightwatchBrowser) => {
7 | await CommonFunction.loginByDev(browser);
8 | browser
9 | // after login go to song-rating create page direct
10 | .url(CONSTANTS.SONGRATING.CREATE)
11 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
12 | // song selection
13 | .assert.containsText('#id_song_container > label', 'Song')
14 | .assert.visible('#id_song_container > div.select-wrapper > input')
15 |
16 | // name input
17 | .assert.containsText('#id_rating_container > label', 'Rating')
18 | .assert.visible('input[id=id_rating]')
19 |
20 | // submit button
21 | .assert.visible('#form-object-songrating > div.modal-footer > button[type=submit]')
22 | .click('#form-object-songrating > div.modal-footer > button[type=submit]')
23 | .pause(CONSTANTS.PAUSE_TIMEOUT)
24 |
25 | .assert.visible('#id_song_container > div > small')
26 | .assert.containsText('#id_song_container > div > small', 'This field is required.')
27 |
28 | .assert.visible('#id_rating_container > div > small')
29 | .assert.containsText('#id_rating_container > div > small', 'This field is required.')
30 |
31 | .end();
32 | }
33 | };
--------------------------------------------------------------------------------
/nightwatch/tests/songs/validation.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../../shared/CONSTANTS';
3 | import { CommonFunction } from '../../shared/commonFunction';
4 |
5 | module.exports = {
6 | 'Song : create song validation': async (browser: NightwatchBrowser) => {
7 | await CommonFunction.loginByDev(browser);
8 | browser
9 | // after login go to song create page direct
10 | .url(CONSTANTS.SONGS.CREATE)
11 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
12 | // artist selection
13 | .assert.containsText('#id_artist_container > label', 'Artist')
14 | .assert.visible('#id_artist_container > div.select-wrapper > input')
15 |
16 | // name input
17 | .assert.containsText('#id_name_container > label', 'Title')
18 | .assert.visible('input[id=id_name]')
19 |
20 | // duration input
21 | .assert.containsText('#id_duration_container > label', 'Duration')
22 | .assert.visible('input[id=id_duration]')
23 | .clearValue('input[id=id_duration]')
24 | .pause(CONSTANTS.PAUSE_TIMEOUT)
25 |
26 | // owner selection
27 | .assert.containsText('#id_owner_container > label', 'Owner')
28 | .assert.visible('#id_owner_container > div.select-wrapper > input')
29 |
30 | // submit button
31 | .assert.visible('#form-object-song > div.modal-footer > button[type=submit]')
32 | .click('#form-object-song > div.modal-footer > button[type=submit]')
33 | .pause(CONSTANTS.PAUSE_TIMEOUT)
34 |
35 | .assert.visible('#id_artist_container > div > small')
36 | .assert.containsText('#id_artist_container > div > small', 'This field is required.')
37 |
38 | .assert.visible('#id_name_container > div > small')
39 | .assert.containsText('#id_name_container > div > small', 'This field is required.')
40 |
41 | .assert.visible('#id_duration_container > div > small')
42 | .assert.containsText('#id_duration_container > div > small', 'This field is required.')
43 |
44 | .assert.visible('#id_owner_container > div > small')
45 | .assert.containsText('#id_owner_container > div > small', 'This field is required.')
46 |
47 | .end();
48 | }
49 | };
--------------------------------------------------------------------------------
/nightwatch/tests/user/validation.ts:
--------------------------------------------------------------------------------
1 | import { NightwatchBrowser } from 'nightwatch';
2 | import { CONSTANTS } from '../../shared/CONSTANTS';
3 | import { CommonFunction } from '../../shared/commonFunction';
4 |
5 | module.exports = {
6 | 'User : create user validation': async (browser: NightwatchBrowser) => {
7 | await CommonFunction.loginByDev(browser);
8 | browser
9 | // after login go to user create page direct
10 | .url(CONSTANTS.USER.CREATE)
11 | .waitForElementVisible('body', CONSTANTS.WAIT_FOR_ELEMENT_VISIBLE_TIMEOUT)
12 |
13 | // blank validation
14 | // username input
15 | .assert.containsText('#id_username_container > label', 'Username')
16 | .assert.visible('input[id=id_username]')
17 |
18 | // email input
19 | .assert.containsText('#id_email_container > label', 'Email address')
20 | .assert.visible('input[id=id_email]')
21 |
22 | // Groups input
23 | .assert.containsText('#id_groups_container > label', 'Groups')
24 | .assert.visible('#id_groups_container > div.select-wrapper > input')
25 |
26 | // staff selection
27 | .assert.containsText('#id_is_staff_container > label > span', 'Staff status')
28 | // .assert.visible('#id_is_staff_container > label > input')
29 |
30 | // Superuser status
31 | .assert.containsText('#id_is_superuser_container > label > span', 'Superuser status')
32 | // .assert.visible('#id_is_superuser_container > label > input')
33 |
34 | // submit button
35 | .assert.visible('#form-object-user > div.modal-footer > button[type=submit]')
36 | .click('#form-object-user > div.modal-footer > button[type=submit]')
37 | .pause(CONSTANTS.PAUSE_TIMEOUT)
38 |
39 | .assert.visible('#id_username_container > div.errors > small')
40 | .assert.containsText('#id_username_container > div.errors > small', 'This field is required.')
41 | .pause(CONSTANTS.PAUSE_TIMEOUT)
42 |
43 | // invalid validation
44 | .pause(CONSTANTS.PAUSE_TIMEOUT)
45 | .assert.visible('input[id=id_username]')
46 | .setValue('input[id=id_username]', CONSTANTS.USER.INVALID_USER)
47 |
48 | // submit button
49 | .assert.visible('#form-object-user > div.modal-footer > button[type=submit]')
50 | .click('#form-object-user > div.modal-footer > button[type=submit]')
51 | .pause(CONSTANTS.PAUSE_TIMEOUT)
52 |
53 | .pause(CONSTANTS.PAUSE_TIMEOUT)
54 | .assert.visible('#id_username_container> div.errors > small')
55 | .assert.containsText('#id_username_container > div.errors > small', 'Enter a valid username. This value may contain only letters, numbers, and @/./+/-/_ characters.')
56 |
57 | // duplicate validation
58 | .pause(CONSTANTS.PAUSE_TIMEOUT)
59 | .assert.visible('input[id=id_username]')
60 | .clearValue('input[id=id_username]')
61 | .setValue('input[id=id_username]', CONSTANTS.USER.EXISTING_USER)
62 |
63 | .pause(CONSTANTS.PAUSE_TIMEOUT)
64 | // submit button
65 | .assert.visible('#form-object-user > div.modal-footer > button[type=submit]')
66 | .click('#form-object-user > div.modal-footer > button[type=submit]')
67 | .pause(CONSTANTS.PAUSE_TIMEOUT)
68 |
69 | .assert.visible('#id_username_container > div.errors > small')
70 | .assert.containsText('#id_username_container > div.errors > small', 'A user with that username already exists.')
71 |
72 | .end();
73 | }
74 | };
--------------------------------------------------------------------------------
/nightwatch/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "es6",
5 | "module": "commonjs",
6 | "outDir": "dist",
7 | "sourceMap": true
8 | },
9 | "include": [
10 | "tests/**/*.ts"
11 | ],
12 | "exclude": [
13 | "node_modules"
14 | ]
15 | }
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE = crudlfap_example.settings
3 | CHANNELS_ENABLE = true
4 | django_debug_mode = keep
5 |
--------------------------------------------------------------------------------
/readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | sphinx:
4 | configuration: docs/conf.py
5 |
6 | build:
7 | os: ubuntu-22.04
8 | tools:
9 | python: "3.12"
10 |
11 | python:
12 | install:
13 | - method: pip
14 | path: .
15 | extra_requirements: [dev]
16 | - requirements: docs/requirements.txt
17 |
18 | formats:
19 | - pdf
20 | - epub
21 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from setuptools import find_packages, setup
5 | from setuptools.command.install import install
6 |
7 | setup(
8 | name='crudlfap',
9 | setup_requires='setupmeta',
10 | versioning='dev',
11 | description='Rich frontend for generic views with Django',
12 | author='James Pic',
13 | author_email='jamespic@gmail.com',
14 | url='https://yourlabs.io/oss/crudlfap',
15 | packages=find_packages('src'),
16 | package_dir={'': 'src'},
17 | include_package_data=True,
18 | keywords='django crud',
19 | install_requires=[
20 | 'ryzom>=0.7.11,<0.8',
21 | 'django>=3.1,<3.2',
22 | 'django-tables2',
23 | 'django-filter',
24 | 'timeago',
25 | 'lookupy-unmanaged',
26 | 'libsass',
27 | ],
28 | tests_require=['tox'],
29 | extras_require=dict(
30 | project=[
31 | 'django-debug-toolbar',
32 | 'django-extensions',
33 | 'django-registration',
34 | 'dj-static',
35 | ],
36 | ),
37 | classifiers=[
38 | 'Development Status :: 1 - Planning',
39 | 'Environment :: Web Environment',
40 | 'Framework :: Django',
41 | 'Intended Audience :: Developers',
42 | 'License :: OSI Approved :: MIT License',
43 | 'Operating System :: OS Independent',
44 | 'Programming Language :: Python',
45 | 'Programming Language :: Python :: 2',
46 | 'Programming Language :: Python :: 3',
47 | 'Topic :: Internet :: WWW/HTTP',
48 | 'Topic :: Software Development :: Libraries :: Python Modules',
49 | ],
50 | python_requires='>=3',
51 | )
52 |
--------------------------------------------------------------------------------
/src/crudlfap/__init__.py:
--------------------------------------------------------------------------------
1 | """Sets the default appconfig to :py:cls:`~crudlfap.apps.DefaultConfig`."""
2 | from django.utils.module_loading import autodiscover_modules
3 |
4 |
5 | def autodiscover():
6 | autodiscover_modules('crudlfap')
7 |
8 |
9 | default_app_config = 'crudlfap.apps.DefaultConfig'
10 |
--------------------------------------------------------------------------------
/src/crudlfap/apps.py:
--------------------------------------------------------------------------------
1 | """Django AppConfig for the crudlfap module."""
2 |
3 | from django import apps
4 | from django.conf import settings
5 |
6 |
7 | def _installed(*apps):
8 | for app in apps:
9 | if app not in settings.INSTALLED_APPS:
10 | return False
11 | return True
12 |
13 |
14 | class DefaultConfig(apps.AppConfig):
15 | """Default AppConfig."""
16 |
17 | name = 'crudlfap'
18 |
19 | def ready(self):
20 | super().ready()
21 | self.module.autodiscover()
22 |
--------------------------------------------------------------------------------
/src/crudlfap/conf.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | from typing import Any, Dict, List, Optional
3 |
4 | SOURCEREF = List[Dict[str, Optional[Dict[str, Any]]]]
5 |
6 | __all__ = (
7 | 'add_optional_dep',
8 | 'install_optional',
9 | )
10 |
11 |
12 | if 'ModuleNotFoundError' not in globals():
13 | ModuleNotFoundError = ImportError
14 |
15 |
16 | def module_installed(module: str) -> bool:
17 | """
18 | Determines if a given module string can be resolved
19 |
20 | Determine if the module referenced by string, can be imported by trying
21 | an import in two ways:
22 |
23 | - direct import of the module
24 | - import of the module minus the last part, then see if the last part is
25 | an attribute of the module.
26 |
27 | Parts of course, are separated by dots.
28 |
29 | :param module: module reference
30 | :return: True if importable, False otherwise
31 | """
32 | have_module = False
33 | try:
34 | importlib.__import__(module)
35 | except ModuleNotFoundError:
36 | mod_path, dot, cls = module.rpartition('.')
37 | if not mod_path:
38 | return False
39 | try:
40 | mod = importlib.import_module(mod_path)
41 | except ModuleNotFoundError:
42 | return False
43 | else:
44 | if hasattr(mod, cls):
45 | have_module = True
46 | else:
47 | have_module = True
48 |
49 | return have_module
50 |
51 |
52 | def add_optional_dep(module: str, to: List[str], before: str = None,
53 | after: str = None):
54 | """
55 | Adds an optional dependency
56 |
57 | Add an optional dependency to the given `to` list, if it is resolvable
58 | by the importer.
59 |
60 | The module can be inserted at the right spot by using before or after
61 | keyword arguments. If both are given, the gun is pointing at your feet
62 | and before wins. If neither are given, the module is appended at the
63 | end.
64 |
65 | :param module: module to add, as it would be added to the given `to` list
66 | :param to: list to add the module to
67 | :param before: module string as should be available in the to list.
68 | :param after: module string as should be available in the to list.
69 | """
70 | if not module_installed(module):
71 | return
72 |
73 | if not before and not after:
74 | to.append(module)
75 | return
76 |
77 | try:
78 | if before:
79 | pos = to.index(before)
80 | else:
81 | pos = to.index(after) + 1
82 | except ValueError:
83 | pass
84 | else:
85 | to.insert(pos, module)
86 |
87 |
88 | def install_optional(source: SOURCEREF, target: List[str]):
89 | """
90 | Install optional modules
91 |
92 | Add a module as provided by the source reference to the target list. The
93 | source reference is a list of dictionaries:
94 |
95 | - key: module reference that needs to be inserted
96 | - value: a dictionary matching the keyword arguments to `add_optional_dep`:
97 | - before: module reference in `target`; the module will be inserted
98 | before this module.
99 | - after: module reference in `target`; the module will be inserted
100 | after this module.
101 | If value is `None`, the module will be appended to `target`.
102 |
103 | :param source: modules to install
104 | :param target: install into this list.
105 | """
106 | for app in source:
107 | for ref, kwargs in app.items():
108 | kwargs = kwargs or {}
109 | add_optional_dep(ref, target, **kwargs)
110 |
--------------------------------------------------------------------------------
/src/crudlfap/crudlfap.py:
--------------------------------------------------------------------------------
1 | from crudlfap.models import URL, Controller
2 | from crudlfap.router import Router
3 | from crudlfap.views import generic
4 |
5 |
6 | class ControllerRouter(Router):
7 | model = Controller
8 |
9 | views = [
10 | generic.DetailView,
11 | generic.ListView.clone(
12 | search_fields=(
13 | 'app',
14 | 'model',
15 | ),
16 | table_fields=(
17 | 'app',
18 | 'model',
19 | ),
20 | ),
21 | ]
22 |
23 |
24 | # useless ?
25 | # ControllerRouter().register()
26 |
27 |
28 | class URLRouter(Router):
29 | model = URL
30 | icon = 'link'
31 |
32 | views = [
33 | generic.DetailView,
34 | generic.ListView.clone(
35 | search_fields=(
36 | 'name',
37 | 'controller__app',
38 | 'controller__model',
39 | ),
40 | table_fields=(
41 | 'controller',
42 | 'id',
43 | 'fullurlpath',
44 | ),
45 | ),
46 | ]
47 |
48 |
49 | URLRouter().register()
50 |
--------------------------------------------------------------------------------
/src/crudlfap/factory.py:
--------------------------------------------------------------------------------
1 | """
2 | **CRIMINALLY INVASIVE HACKS** in :py:class:`Factory`.
3 | """
4 | import inspect
5 |
6 |
7 | class FactoryMetaclass(type):
8 | """``__getattr__`` that ensures a first argument to getters.
9 |
10 | Makes the getter work both from class and instance
11 |
12 | Thanks to this, your `get_*()` methods will /maybe/ work in both
13 | cases::
14 |
15 | YourClass.foo # calls get_foo(YourClass)
16 | YourClass().foo # calls get_foo(self)
17 |
18 | Don't code drunk.
19 | """
20 |
21 | def __getattr__(cls, attr):
22 | if attr.startswith('get_'):
23 | raise AttributeError('{} or {}'.format(attr[4:], attr))
24 |
25 | getter = getattr(cls, 'get_' + attr)
26 |
27 | if inspect.ismethod(getter):
28 | return getter()
29 | else:
30 | return getter(cls)
31 |
32 | def get_cls(cls):
33 | """Return the cls.
34 |
35 | did it go to far at this point ?
36 | """
37 | return cls
38 |
39 |
40 | class Factory(metaclass=FactoryMetaclass):
41 | """Adds clumsy but automatic getter resolving.
42 |
43 | The `__getattr__` override makes this class try to call a get_*() method
44 | for variables that are not in `self.__dict__`.
45 |
46 | For example, when `self.foo` is evaluated `and 'foo' not in self.__dict__`
47 | then it will call the `self.get_foo()`
48 |
49 | If `self.get_foo()` returns None, it will try to get the result again from
50 | `self.__dict__`. Which means that we are going to witness this horrroorr::
51 |
52 | class YourEvil(Factory):
53 | def get_foo(self):
54 | self.calls += 1
55 | self.foo = 13
56 |
57 | assert YourEvil.foo == 13 # crime scene 1
58 | assert YourEvil.foo == 13 # crime scene 2
59 | assert YourEvil.calls == 1 # crime scene 3
60 |
61 | For the moment it is pretty clumsy because i tried to contain the
62 | criminality rate as low as possible meanwhile i like the work it does for
63 | me !
64 | """
65 | def __getattr__(self, attr):
66 | if attr.startswith('get_'):
67 | raise AttributeError('{} or {}()'.format(attr[4:], attr))
68 |
69 | getter = getattr(self, 'get_{}'.format(attr), None)
70 |
71 | if getter:
72 | methresult = getter()
73 | dictresult = self.__dict__.get(attr, None)
74 | if methresult is None and dictresult is not None:
75 | result = dictresult
76 | else:
77 | result = methresult
78 | return result
79 |
80 | # Try class methods
81 | return getattr(type(self), attr)
82 |
83 | @classmethod
84 | def clone(cls, *mixins, **attributes):
85 | """Return a subclass with the given attributes.
86 |
87 | If a model is found, it will prefix the class name with the model.
88 | """
89 | name = cls.__name__
90 | model = attributes.get('model', getattr(cls, 'model', None))
91 | if model is None:
92 | model = getattr(cls, 'model', None)
93 | if model and model.__name__ not in cls.__name__:
94 | name = '{}{}'.format(model.__name__, cls.__name__)
95 | return type(name, (cls,) + mixins, attributes)
96 |
--------------------------------------------------------------------------------
/src/crudlfap/locale/fr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: django\n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2018-11-14 12:11+0100\n"
6 | "PO-Revision-Date: 2017-01-21 14:47+0000\n"
7 | "Last-Translator: Claude Paroz \n"
8 | "Language-Team: French (http://www.transifex.com/django/django/language/fr/)\n"
9 | "Language: fr\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
14 |
15 | #: src/crudlfap/jinja2/crudlfap/_breadcrumb_object.html:1
16 | #: src/crudlfap/jinja2/crudlfap/_menu_main.html:2
17 | msgid "Home"
18 | msgstr "Accueil"
19 |
20 | #: src/crudlfap/jinja2/crudlfap/_menu_auth.html:13
21 | msgid "Back to your account"
22 | msgstr "Retour vers votre compte"
23 |
24 | #: src/crudlfap/jinja2/crudlfap/_modal.html:5
25 | msgid "Close"
26 | msgstr "Fermer"
27 |
28 | #: src/crudlfap/jinja2/crudlfap/form.html:28
29 | msgid "Objects ignored because of missing authorization"
30 | msgstr "Objets ignorés faute d'autorisation"
31 |
32 | #: src/crudlfap/jinja2/crudlfap/form.html:37
33 | msgid "cancel"
34 | msgstr "annuler"
35 |
36 | #: src/crudlfap/jinja2/crudlfap/list.html:50
37 | msgid "That page contains no results"
38 | msgstr "Cette page ne contient aucun résultat"
39 |
40 | #: src/crudlfap/mixins/lock.py:41
41 | msgid "Page was open by {} {}"
42 | msgstr "Page ouverte par {} {}"
43 |
44 | #: src/crudlfap/mixins/lock.py:42
45 | msgid "unknown user"
46 | msgstr "utilisateur inconnu"
47 |
48 | #: src/crudlfap/mixins/modelform.py:45
49 | msgid "failure"
50 | msgstr "échec"
51 |
52 | #: src/crudlfap/mixins/modelform.py:66
53 | msgid "detail"
54 | msgstr "détail"
55 |
56 | #: src/crudlfap/mixins/search.py:28
57 | msgid "Search"
58 | msgstr "Rechercher"
59 |
60 | msgid "home"
61 | msgstr "accueil"
62 |
63 | msgid "create"
64 | msgstr "ajouter"
65 |
66 | msgid "Create"
67 | msgstr "Création"
68 |
69 | msgid "delete"
70 | msgstr "effacer"
71 |
72 | msgid "update"
73 | msgstr "modifier"
74 |
75 | msgid "Update"
76 | msgstr "Modifier"
77 |
78 | msgid "list"
79 | msgstr "liste"
80 |
81 | msgid "List"
82 | msgstr "Liste"
83 |
84 | msgid "Select action to run on selected items"
85 | msgstr "Selectionner l'action a executer sur les lignes cochées"
86 |
--------------------------------------------------------------------------------
/src/crudlfap/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap/management/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap/management/commands/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap/mixins/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa: F401
2 |
3 | from .crud import (ActionMixin, CreateMixin, DeleteMixin, DetailMixin,
4 | HistoryMixin, ListMixin, UpdateMixin)
5 | from .filter import FilterMixin
6 | from .form import FormMixin
7 | from .lock import LockMixin
8 | from .menu import MenuMixin
9 | from .model import ModelMixin
10 | from .modelform import ModelFormMixin, log, log_insert
11 | from .object import ObjectMixin
12 | from .objectform import ObjectFormMixin
13 | from .objects import ObjectsMixin
14 | from .objectsform import ObjectsFormMixin
15 | from .search import SearchMixin
16 | from .table import TableMixin
17 | from .template import TemplateMixin
18 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/filter.py:
--------------------------------------------------------------------------------
1 | import django_filters
2 | from django import forms
3 | from django.db import models
4 |
5 |
6 | class FilterMixin(object):
7 | def get_filter_model(self):
8 | return self.filterset_kwargs['queryset'].model
9 |
10 | def get_filterset(self):
11 | """
12 | Returns an instance of the filterset to be used in this view.
13 | """
14 | self.filterset = self.filterset_class(**self.filterset_kwargs)
15 |
16 | # filter out choices which have no result to avoid filter pollution
17 | # with choices which would empty out results
18 | for name, field in self.filterset.form.fields.items():
19 | try:
20 | mf = self.filter_model._meta.get_field(name)
21 | except Exception:
22 | continue
23 |
24 | if not isinstance(mf, models.ForeignKey):
25 | continue
26 |
27 | field.queryset = field.queryset.annotate(
28 | c=models.Count(mf.related_query_name())
29 | ).filter(c__gt=0)
30 |
31 | def get_filterset_data(self):
32 | return self.request.GET.copy()
33 |
34 | def get_filterset_kwargs(self):
35 | """
36 | Returns the keyword arguments for instanciating the filterset.
37 | """
38 | return {
39 | 'data': self.filterset_data,
40 | 'request': self.request,
41 | 'queryset': self.queryset,
42 | }
43 |
44 | def get_filterset_meta_filter_overrides(self):
45 | return {
46 | models.CharField: {
47 | 'filterset_class': django_filters.CharFilter, # noqa
48 | 'extra': lambda f: {
49 | 'lookup_expr': 'icontains',
50 | },
51 | },
52 | }
53 |
54 | def get_filter_fields(self):
55 | return []
56 |
57 | def get_filterset_form_class(self):
58 | return type(
59 | 'FiltersetForm',
60 | (forms.Form,),
61 | {
62 | '_layout': self.filterset_form_layout,
63 | }
64 | )
65 |
66 | def get_filterset_form_layout(self):
67 | return None
68 |
69 | def get_filterset_meta_attributes(self):
70 | return dict(
71 | model=self.filter_model,
72 | fields=self.filter_fields,
73 | filter_overrides=self.filterset_meta_filter_overrides,
74 | form=self.filterset_form_class,
75 | )
76 |
77 | def get_filterset_meta_class(self):
78 | return type('Meta', (object,), self.filterset_meta_attributes)
79 |
80 | def get_filterset_extra_class_attributes(self):
81 | extra = dict()
82 | for field_name in self.filter_fields:
83 | try:
84 | field = self.filter_model._meta.get_field(field_name)
85 | except: # noqa
86 | continue
87 | choices = getattr(field, 'choices', None)
88 | if choices is None:
89 | continue
90 | extra[field_name] = django_filters.MultipleChoiceFilter(
91 | choices=choices)
92 | return extra
93 |
94 | def get_filterset_class_attributes(self):
95 | res = dict(Meta=self.filterset_meta_class)
96 | res.update(self.filterset_extra_class_attributes)
97 | return res
98 |
99 | def get_filterset_class(self):
100 | return type(
101 | '{}FilterSet'.format(self.filter_model.__name__),
102 | (django_filters.FilterSet,),
103 | self.filterset_class_attributes
104 | )
105 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/form.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django import http
4 | from django.contrib import messages
5 |
6 |
7 | class FormMixin:
8 | """Mixin for views which have a Form."""
9 |
10 | initial = {}
11 | default_template_name = 'crudlfap/form.html'
12 |
13 | def get_context(self, **context):
14 | context['form'] = self.form
15 | return super().get_context(**context)
16 |
17 | def form_valid(self):
18 | """If the form is valid, redirect to the supplied URL."""
19 | if self.request.content_type == 'application/json':
20 | return self.form_valid_json()
21 |
22 | self.message_success()
23 | return http.HttpResponseRedirect(self.success_url)
24 |
25 | def form_valid_json(self):
26 | return http.JsonResponse({
27 | 'data': self.form.cleaned_data,
28 | 'status': 'accepted',
29 | }, status=201)
30 |
31 | def form_invalid_json(self):
32 | data = dict(
33 | status='invalid data',
34 | non_field_errors=self.form.non_field_errors(),
35 | field_errors=dict(),
36 | )
37 | for name, errors in self.form.errors.items():
38 | data['field_errors'][name] = errors
39 | return http.JsonResponse(data, status=405)
40 |
41 | def form_invalid(self):
42 | """If the form is invalid, render the invalid form."""
43 | if self.request.content_type == 'application/json':
44 | return self.form_invalid_json()
45 |
46 | self.message_error()
47 | response = self.render_to_response()
48 | response.status_code = 400
49 | return response
50 |
51 | def get_title_submit(self):
52 | """
53 | Title of the submit button.
54 |
55 | Defaults to :py:attr:`~crudlfap.mixins.menu.MenuMixin.title_menu`
56 | """
57 | return self.title_menu
58 |
59 | def post(self, request, *args, **kwargs):
60 | """
61 | Handle POST requests: instantiate a form instance with the passed
62 | POST variables and then check if it's valid.
63 | """
64 | if self.form.is_valid():
65 | return self.form_valid()
66 | else:
67 | return self.form_invalid()
68 |
69 | def get_initial(self):
70 | """Return the initial data to use for forms on this view."""
71 | return self.initial.copy()
72 |
73 | def get_prefix(self):
74 | """Return the prefix to use for forms."""
75 | return None
76 |
77 | def get_form_class(self):
78 | if self.router:
79 | return getattr(self.router, 'form_class')
80 |
81 | def get_form(self):
82 | """Return an instance of the form to be used in this view."""
83 | self.form = self.form_class(**self.get_form_kwargs())
84 | return self.form
85 |
86 | def get_form_kwargs(self):
87 | """Return the keyword arguments for instantiating the form."""
88 | self.form_kwargs = {
89 | 'initial': self.get_initial(),
90 | 'prefix': self.get_prefix(),
91 | }
92 |
93 | if self.request.method in ('POST', 'PUT'):
94 | if self.request.content_type == 'application/json':
95 | data = json.loads(self.request.body)
96 | else:
97 | data = self.request.POST
98 | self.form_kwargs.update({
99 | 'data': data,
100 | 'files': self.request.FILES,
101 | })
102 | return self.form_kwargs
103 |
104 | def get_next_url(self):
105 | if '_next' in self.request.POST:
106 | self.next_url = self.request.POST.get('_next')
107 | if '_next' in self.request.GET:
108 | self.next_url = self.request.GET.get('_next')
109 |
110 | def get_success_url(self):
111 | if self.next_url:
112 | return self.next_url
113 | if self.router['list']:
114 | return self.router['list'].url
115 | return super().get_success_url()
116 |
117 | def message_html(self, message):
118 | return message
119 |
120 | def message_success(self):
121 | messages.success(
122 | self.request,
123 | self.message_html(self.form_valid_message)
124 | )
125 |
126 | def message_error(self):
127 | messages.error(
128 | self.request,
129 | self.message_html(self.form_invalid_message)
130 | )
131 |
132 | def get_swagger_summary(self):
133 | return self.title
134 |
135 | def get_swagger_post(self):
136 | result = {
137 | 'consumes': ['application/json'],
138 | # 'description': self.title,
139 | # 'operationId': 'addPet',
140 | 'parameters': [],
141 | 'produces': ['application/json', 'application/xml'],
142 | 'responses': {
143 | '400': {
144 | 'description': 'Invalid input'
145 | },
146 | },
147 | 'summary': self.swagger_summary,
148 | 'tags': self.swagger_tags,
149 | }
150 | for name, field in self.form_class.base_fields.items():
151 | result['parameters'].append({
152 | 'description': '. '.join([
153 | str(field.label),
154 | str(field.help_text)
155 | ]),
156 | 'name': name,
157 | 'required': field.required,
158 | # 'schema': {'$ref': '#/definitions/Pet'}
159 | })
160 | return result
161 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/lock.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import timeago
4 | from django.conf import settings
5 | from django.contrib import messages
6 | from django.core.cache import cache
7 | from django.utils import timezone
8 | from django.utils.translation import gettext_lazy as _
9 |
10 |
11 | class LockMixin(object):
12 | """Currently implements soft lock."""
13 | locks = False
14 |
15 | def get_deadlock_delta(self):
16 | return datetime.timedelta(**self.deadlock_delta_kwargs)
17 |
18 | def get_deadlock_delta_kwargs(self):
19 | return dict(seconds=30)
20 |
21 | def get_lock_key(self):
22 | return 'lock-{}'.format(self.url)
23 |
24 | def get_lock_value(self):
25 | return cache.get(self.lock_key)
26 |
27 | def get_locked(self):
28 | if not self.locks:
29 | return False
30 | value = self.lock_value
31 | if not value:
32 | return False
33 | if value['datetime'] + self.deadlock_delta < timezone.now():
34 | # expired lock
35 | cache.delete(self.lock_key)
36 | return False
37 | return True
38 |
39 | def get_locked_message(self):
40 | return _('Page was open by {} {}').format(
41 | self.lock_value.get('username', _('unknown user')),
42 | timeago.format(
43 | timezone.now() - self.lock_value['datetime'],
44 | getattr(self.request, 'LANGUAGE_CODE', settings.LANGUAGE_CODE)
45 | )
46 | )
47 |
48 | def get(self, request, *args, **kwargs):
49 | if self.locked:
50 | messages.warning(request, self.locked_message)
51 | if self.locks:
52 | self.lock()
53 | return super().get(request, *args, **kwargs)
54 |
55 | def lock(self):
56 | value = dict(datetime=timezone.now())
57 | if self.request.user.is_authenticated:
58 | value['username'] = self.request.user.username
59 | cache.set(self.lock_key, value)
60 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/menu.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import gettext_lazy as _
2 |
3 |
4 | class MenuMixin(object):
5 | def get_title_menu(self):
6 | """Return title for menu links to this view."""
7 | return _(self.view_label).capitalize()
8 |
9 | def get_menu(self):
10 | return None
11 |
12 | def get_menu_kwargs(self):
13 | return dict()
14 |
15 | def get_menu_views(self):
16 | views = []
17 | for view in self.router.views:
18 | for menu in view.menus:
19 | if menu in self.menus_display:
20 | view = view.clone(
21 | request=self.request,
22 | **self.menu_kwargs,
23 | )
24 |
25 | if not view().has_perm():
26 | continue
27 | if view.urlname == self.urlname:
28 | continue
29 | if view.urlname in [v.urlname for v in views]:
30 | continue
31 | views.append(view)
32 | return views
33 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/model.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 | from django.utils.translation import gettext as _
3 |
4 |
5 | class ModelMixin(object):
6 | """Mixin for views using a Model class but no instance."""
7 | model = None
8 |
9 | menus = ['model']
10 | menus_display = ['model']
11 | pluralize = False
12 |
13 | def get_exclude(self):
14 | return []
15 |
16 | def get_fields(self):
17 | return [
18 | f for f in self.router.get_fields(self)
19 | if self.model._meta.get_field(f).editable
20 | and f not in self.exclude
21 | ]
22 |
23 | def get_model_verbose_name(self):
24 | if self.pluralize:
25 | return self.model._meta.verbose_name_plural
26 | else:
27 | return self.model._meta.verbose_name
28 |
29 | def get_title(self):
30 | """Compose a title of Model Name: View label."""
31 | return '{}: {}'.format(
32 | self.model_verbose_name.capitalize(),
33 | _(self.view_label),
34 | ).capitalize()
35 |
36 | def get_queryset(self):
37 | """Return router.get_queryset() by default, otherwise super()."""
38 | if self.router:
39 | return self.router.get_queryset(self)
40 |
41 | if self.model:
42 | return self.model._default_manager.all()
43 |
44 | raise ImproperlyConfigured(
45 | "%(cls)s is missing a QuerySet. Define "
46 | "%(cls)s.model, %(cls)s.queryset, or override "
47 | "%(cls)s.get_queryset()." % {
48 | 'cls': self.__class__.__name__
49 | }
50 | )
51 |
52 | def get_swagger_model_name(self):
53 | if self.model:
54 | return self.model.__name__
55 |
56 | def get_swagger_tags(self):
57 | if self.model:
58 | return [self.model.__name__]
59 | return []
60 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/modelform.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from django import http
4 | from django.contrib.admin.models import LogEntry
5 | from django.contrib.contenttypes.models import ContentType
6 | from django.forms import models as model_forms
7 | from django.utils.translation import gettext as _
8 |
9 | from .form import FormMixin
10 | from .model import ModelMixin
11 |
12 |
13 | def log(user, flag, message, obj=None, dt=None):
14 | from django.contrib.admin.models import ADDITION, CHANGE, DELETION
15 | flags = dict(
16 | create=ADDITION,
17 | update=CHANGE,
18 | delete=DELETION
19 | )
20 | flag = flags.get(flag, flag)
21 | if not isinstance(message, str):
22 | message = json.dumps(message)
23 | logentry = LogEntry(
24 | user_id=user.pk,
25 | action_flag=flag,
26 | change_message=message,
27 | )
28 | if obj:
29 | logentry.content_type_id = ContentType.objects.get_for_model(obj).pk
30 | logentry.object_id = obj.pk
31 | logentry.object_repr = str(obj)[:200]
32 | if dt:
33 | logentry.action_time = dt
34 | logentry.save()
35 | return logentry
36 |
37 |
38 | log_insert = log # backward compat
39 |
40 |
41 | class ModelFormMixin(ModelMixin, FormMixin):
42 | """ModelForm Mixin using readable"""
43 | menus = ['model']
44 |
45 | def form_valid_json(self):
46 | return http.JsonResponse({
47 | 'data': self.router.serialize(self.object),
48 | 'status': 'created',
49 | }, status=201)
50 |
51 | def get_form_kwargs(self):
52 | self.form_kwargs = super().get_form_kwargs()
53 | if (
54 | hasattr(self, 'object')
55 | and issubclass(self.form_class, model_forms.ModelForm)
56 | ):
57 | self.form_kwargs.update({'instance': self.object})
58 | return self.form_kwargs
59 |
60 | def get_form(self):
61 | self.form = self.form_class(**self.form_kwargs)
62 | return self.form
63 |
64 | def get_form_fields(self):
65 | return self.fields
66 |
67 | def get_form_base(self):
68 | return None
69 |
70 | def get_modelform_kwargs(self):
71 | kwargs = dict()
72 | if self.form_base:
73 | kwargs['form'] = self.form_base
74 | kwargs['fields'] = self.form_fields
75 | return kwargs
76 |
77 | def get_form_class(self):
78 | return model_forms.modelform_factory(
79 | self.model,
80 | **self.modelform_kwargs
81 | )
82 |
83 | def get_form_invalid_message(self):
84 | return '{}: {}: {}'.format(
85 | _(self.view_label),
86 | self.model_verbose_name,
87 | _('failure'),
88 | ).capitalize()
89 |
90 | def get_form_valid_message(self):
91 | return '{}: {}'.format(
92 | _(self.view_label),
93 | self.form.instance,
94 | ).capitalize()
95 |
96 | def message_html(self, message):
97 | return message
98 |
99 | def get_log_action_flag(self):
100 | return False
101 |
102 | def get_log_message(self):
103 | return _(self.view_label)
104 |
105 | def log_insert(self):
106 | if not LogEntry:
107 | return
108 |
109 | if not self.request.user.is_authenticated:
110 | return
111 |
112 | if not self.log_action_flag:
113 | return
114 |
115 | if hasattr(self, 'object_list'):
116 | objects = self.object_list
117 | elif hasattr(self, 'log_objects'):
118 | objects = self.log_objects
119 | else:
120 | objects = [self.object]
121 |
122 | for obj in objects:
123 | log_insert(
124 | self.request.user,
125 | self.log_action_flag,
126 | self.log_message,
127 | obj,
128 | )
129 |
130 | def form_valid(self):
131 | response = super().form_valid()
132 | self.log_insert()
133 | return response
134 |
135 | def get_success_url(self):
136 | if self.next_url:
137 | return self.next_url
138 |
139 | if hasattr(self.object, 'get_absolute_url'):
140 | return self.object.get_absolute_url()
141 |
142 | return super().get_success_url()
143 |
144 | def get_swagger_summary(self):
145 | return f'{self.model.__name__} {self.label}'
146 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/object.py:
--------------------------------------------------------------------------------
1 | from django import http
2 | from django.conf import settings
3 | from django.contrib.admin.models import LogEntry
4 | from django.contrib.contenttypes.models import ContentType
5 | from django.core.exceptions import ImproperlyConfigured
6 | from django.db import models
7 | from django.db.models import Q
8 | from django.utils.translation import gettext_lazy as _
9 |
10 | from .model import ModelMixin
11 |
12 |
13 | class ObjectMixin(ModelMixin):
14 | """Mixin for views using a Model instance."""
15 |
16 | menus = ['object', 'object_detail']
17 | menus_display = ['object', 'object_detail']
18 | template_name_field = None
19 |
20 | def logentries(self):
21 | filters = Q(
22 | content_type=ContentType.objects.get_for_model(self.model),
23 | object_id=self.object.pk,
24 | )
25 | name = '.'.join([
26 | self.object._meta.app_label,
27 | self.object._meta.model_name
28 | ])
29 | if name.lower() == settings.AUTH_USER_MODEL.lower():
30 | filters |= Q(user_id=self.object.pk)
31 | return LogEntry.objects.filter(filters).order_by('-action_time')
32 |
33 | def get_context(self, **context):
34 | context['object'] = self.object
35 | return super().get_context(**context)
36 |
37 | def get_template_name_suffix(self):
38 | return '_{}'.format(self.urlname)
39 |
40 | def get_urlargs(self):
41 | """Return list with object's urlfield attribute."""
42 | return [getattr(self.object, self.urlfield)]
43 |
44 | @classmethod
45 | def to_url_args(cls, *args):
46 | """Return first arg's url_field attribute."""
47 | url_field = cls.get_url_field()
48 | return [getattr(args[0], url_field)]
49 |
50 | @classmethod
51 | def get_urlpath(cls):
52 | """Identify the object by slug or pk in the pattern."""
53 | return r'<{}>/{}'.format(cls.urlfield, cls.urlname)
54 |
55 | def get_title(self):
56 | return '{} "{}": {}'.format(
57 | self.model_verbose_name,
58 | self.object,
59 | _(self.view_label).capitalize(),
60 | ).capitalize()
61 |
62 | def get_menu_kwargs(self):
63 | return dict(object=self.object)
64 |
65 | def get_object(self, queryset=None):
66 | """
67 | Return the object the view is displaying.
68 |
69 | Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
70 | Subclasses can override this to return any object.
71 | """
72 | if getattr(self, 'kwargs', False) is False:
73 | # This happens when the view has not been instanciated with an
74 | # object, neither from a URL which would allow getting the object
75 | # in the super() call below.
76 | raise Exception('Must instanciate the view with an object')
77 |
78 | # Use a custom queryset if provided; this is required for subclasses
79 | # like DateDetailView
80 | if queryset is None:
81 | queryset = self.get_queryset()
82 |
83 | queryset = queryset.filter(
84 | **{self.urlfield: self.kwargs.get(self.urlfield)}
85 | )
86 |
87 | try:
88 | # Get the single item from the filtered queryset
89 | obj = queryset.get()
90 | except queryset.model.DoesNotExist:
91 | raise http.Http404(
92 | _("No %(verbose_name)s found matching the query") %
93 | {'verbose_name': queryset.model._meta.verbose_name}
94 | )
95 |
96 | return obj
97 |
98 | def object_get(self):
99 | """Return the object, uses get_object() if necessary."""
100 | cached = getattr(self, '_object', None)
101 | if not cached:
102 | self._object = self.get_object()
103 | return self._object
104 |
105 | def object_set(self, value):
106 | """Set self.object attribute."""
107 | self._object = value
108 |
109 | object = property(object_get, object_set)
110 |
111 | def get_template_names(self):
112 | """
113 | Return a list of template names to be used for the request.
114 |
115 | May not be called if render_to_response() is overridden. Return the
116 | following list:
117 |
118 | * the value of ``template_name`` on the view (if provided)
119 | * the contents of the ``template_name_field`` field on the
120 | object instance that the view is operating upon (if available)
121 | * ``/.html``
122 | """
123 | try:
124 | names = super().get_template_names()
125 | except (ImproperlyConfigured, AttributeError):
126 | # If template_name isn't specified, it's not a problem --
127 | # we just start with an empty list.
128 | names = []
129 |
130 | # If self.template_name_field is set, grab the value of the field
131 | # of that name from the object; this is the most specific template
132 | # name, if given.
133 | if self.object and self.template_name_field:
134 | name = getattr(self.object, self.template_name_field, None)
135 | if name:
136 | names.insert(0, name)
137 |
138 | # The least-specific option is the default
139 | # /_detail.html; only use this if the object in
140 | # question is a model.
141 | if isinstance(self.object, models.Model):
142 | object_meta = self.object._meta
143 | names.append("%s/%s%s.html" % (
144 | object_meta.app_label,
145 | object_meta.model_name,
146 | self.template_name_suffix
147 | ))
148 | elif self.model and issubclass(self.model, models.Model):
149 | names.append("%s/%s%s.html" % (
150 | self.model._meta.app_label,
151 | self.model._meta.model_name,
152 | self.template_name_suffix
153 | ))
154 |
155 | # If we still haven't managed to find any template names, we should
156 | # re-raise the ImproperlyConfigured to alert the user.
157 | if not names:
158 | raise
159 |
160 | return names
161 |
162 | def get_abstract_object(self):
163 | return self.model()
164 |
165 | @classmethod
166 | def abstract(cls, **kwargs):
167 | if 'object' not in kwargs:
168 | kwargs['object'] = cls.abstract_object
169 | return cls(**kwargs)
170 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/objectform.py:
--------------------------------------------------------------------------------
1 | from django.contrib.admin.models import CHANGE
2 | from django.utils.translation import gettext as _
3 |
4 | from .modelform import ModelFormMixin
5 | from .object import ObjectMixin
6 |
7 |
8 | class ObjectFormMixin(ObjectMixin, ModelFormMixin):
9 | """Custom form view mixin on an object."""
10 | log_action_flag = CHANGE
11 |
12 | def get_form_valid_message(self):
13 | return _(
14 | '%s %s: {}' % (_(self.view_label), self.model_verbose_name)
15 | ).format(self.object).capitalize()
16 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/objects.py:
--------------------------------------------------------------------------------
1 | from .model import ModelMixin
2 |
3 |
4 | class ObjectsMixin(ModelMixin):
5 | pluralize = True
6 |
7 | def get_objects(self):
8 | return self.queryset
9 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/objectsform.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import gettext as _
2 |
3 | from .modelform import ModelFormMixin
4 |
5 |
6 | class ObjectsFormMixin(ModelFormMixin):
7 | pluralize = True
8 | menus = ['list_action']
9 |
10 | def get_invalid_pks(self):
11 | return len(self.request.GET.getlist('pk')) - len(self.object_list)
12 |
13 | def get_object_list(self):
14 | self.object_list = self.queryset.filter(
15 | pk__in=self.request.GET.getlist('pk')
16 | )
17 | return self.object_list
18 |
19 | def get_success_url(self):
20 | return self.router['list'].reverse()
21 |
22 | def get_form_valid_message(self):
23 | return '{}: {}'.format(
24 | _(self.view_label),
25 | ', '.join([str(o) for o in self.object_list]),
26 | ).capitalize()
27 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/search.py:
--------------------------------------------------------------------------------
1 | import operator
2 | from functools import reduce
3 |
4 | from django import forms
5 | from django.db import models
6 | from django.db.models import Q
7 | from django.utils.translation import gettext as _
8 |
9 |
10 | class SearchForm(forms.Form):
11 | q = forms.CharField(label=_('Search'), required=False)
12 |
13 |
14 | class SearchMixin(object):
15 | def get_search_model(self):
16 | return self.model
17 |
18 | def get_search_fields(self):
19 | if hasattr(self.router, 'search_fields'):
20 | return self.router.search_fields
21 | return [
22 | f.name
23 | for f in self.search_model._meta.fields
24 | if isinstance(f, models.CharField)
25 | ]
26 |
27 | def get_search_form_class(self):
28 | if not self.search_fields:
29 | return
30 |
31 | return SearchForm
32 |
33 | def get_search_form(self):
34 | if self.search_fields:
35 | self.search_form = self.search_form_class(
36 | self.request.GET,
37 | )
38 | self.search_form.full_clean()
39 | else:
40 | self.search_form = None
41 | return self.search_form
42 |
43 | def search_filter(self, qs):
44 | q = self.search_form.cleaned_data.get('q', '')
45 |
46 | if not self.search_fields or not q:
47 | return qs
48 |
49 | return qs.filter(reduce(
50 | operator.or_,
51 | [
52 | Q(**{search_field + '__icontains': q})
53 | for search_field in self.search_fields
54 | ]
55 | )).distinct()
56 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/table.py:
--------------------------------------------------------------------------------
1 | import collections
2 |
3 | import django_tables2 as tables
4 | from django.db import models
5 | from django.utils.safestring import mark_safe
6 | from django.utils.translation import gettext_lazy as _
7 | from django_tables2.config import RequestConfig
8 |
9 | from crudlfap import html
10 |
11 |
12 | class UnpolyLinkColumn(tables.LinkColumn):
13 | def render(self, record, value):
14 | return super().render(record, value).replace(
15 | ' self.max_per_page:
153 | return self.max_per_page
154 | return dict(per_page=per_page)
155 |
156 | def get_table(self):
157 | kwargs = self.table_kwargs
158 | self.table = self.build_table_class()(**kwargs)
159 | RequestConfig(
160 | self.request,
161 | paginate=self.table_pagination,
162 | ).configure(self.table)
163 | return self.table
164 |
165 | def get_paginate_by(self):
166 | return 10
167 |
--------------------------------------------------------------------------------
/src/crudlfap/mixins/template.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.conf import settings
4 | from django.template.response import TemplateResponse
5 | from django.utils.translation import gettext as _
6 |
7 | logger = logging.getLogger()
8 |
9 |
10 | class TemplateMixin:
11 | """
12 | Replacement for Django's TemplateResponseMixin.
13 |
14 | This allows to configure "last resort" templates for each class, and thus
15 | to provide a working CRUD out of the box.
16 | """
17 |
18 | def get(self, request, *args, **kwargs):
19 | return self.render_to_response()
20 |
21 | def render_to_response(self):
22 | """
23 | Return a response, using the `response_class` for this view, with a
24 | template rendered with the given context.
25 |
26 | Pass response_kwargs to the constructor of the response class.
27 | """
28 | return self.response_class(
29 | request=self.request,
30 | template=self.template_names,
31 | context=self.context,
32 | using=self.template_engine,
33 | **self.response_kwargs
34 | )
35 |
36 | def get_template_engine(self):
37 | return getattr(
38 | settings, 'CRUDLFAP', {}
39 | ).get('TEMPLATE_ENGINE', 'ryzom')
40 |
41 | def get_response_class(self):
42 | return TemplateResponse
43 |
44 | def get_response_kwargs(self):
45 | return dict(content_type='text/html')
46 |
47 | def get_context(self, **context):
48 | context['view'] = self
49 | self.context = context
50 |
51 | def get_view_label(self):
52 | return self.label
53 |
54 | def get_title(self):
55 | return _(self.view_label).capitalize()
56 |
57 | def get_title_link(self):
58 | """Return title attribute for links to this view."""
59 | return self.title
60 |
61 | def get_title_html(self):
62 | """Return text for HTML title tag."""
63 | return self.title
64 |
65 | def get_title_heading(self):
66 | """Return text for page heading."""
67 | return self.title
68 |
69 | def get_template_name_suffix(self):
70 | return self.urlname
71 |
72 | def get_template_name_suffixes(self):
73 | return [self.template_name_suffix]
74 |
75 | def get_template_names(self):
76 | """Give a chance to default_template_name."""
77 | template_names = []
78 | template_name = getattr(self, 'template_name', None)
79 | if template_name:
80 | template_names.append(template_name)
81 |
82 | if getattr(self, 'model', None):
83 | template_names.append('%s/%s%s.html' % (
84 | self.model._meta.app_label,
85 | self.model._meta.model_name,
86 | '_' + self.urlname,
87 | ))
88 |
89 | default_template_name = getattr(self, 'default_template_name', None)
90 | if default_template_name:
91 | template_names.append(default_template_name)
92 | return template_names
93 |
--------------------------------------------------------------------------------
/src/crudlfap/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import Permission
2 | from django.contrib.contenttypes.models import ContentType
3 | from django.db import models
4 | from lookupy import Collection
5 |
6 | from crudlfap.site import site as default_site
7 |
8 |
9 | class ControllerManager(models.Manager):
10 | def __init__(self, site=None):
11 | super().__init__()
12 | self.site = site or default_site
13 |
14 | def get_queryset(self):
15 | items = [
16 | Controller.factory(i) for i in self.site.values()
17 | ]
18 | return Collection(items)
19 |
20 |
21 | class Controller(models.Model):
22 | router = models.Field()
23 | model = models.Field()
24 | app = models.Field()
25 |
26 | objects = ControllerManager()
27 |
28 | class Meta:
29 | managed = False
30 |
31 | def __str__(self):
32 | return self.pk
33 |
34 | @classmethod
35 | def factory(cls, router):
36 | return cls(
37 | pk=f'{router.model._meta.app_label}.{router.model.__name__}',
38 | router=router,
39 | app=str(router.model._meta.app_config.verbose_name),
40 | model=str(router.model._meta.verbose_name).capitalize(),
41 | )
42 |
43 |
44 | class URLManager(models.Manager):
45 | def __init__(self, site=None):
46 | super().__init__()
47 | self.site = site or default_site
48 |
49 | def get_queryset(self):
50 | views = [
51 | URL.factory(i) for i in self.site.views
52 | ]
53 |
54 | for router in self.site.values():
55 | views += [
56 | URL.factory(i) for i in router.views
57 | ]
58 |
59 | return Collection(views)
60 |
61 |
62 | class URL(models.Model):
63 | controller = models.ForeignKey(
64 | Controller,
65 | on_delete=models.SET_NULL,
66 | null=True,
67 | )
68 | urlpath = models.Field()
69 | fullurlpath = models.Field()
70 | urlname = models.Field()
71 | urlfullname = models.Field()
72 | view = models.Field()
73 | model = models.Field()
74 |
75 | objects = URLManager()
76 |
77 | class Meta:
78 | managed = False
79 |
80 | def __str__(self):
81 | return self.view.label
82 |
83 | @property
84 | def content_type(self):
85 | return ContentType.objects.get_for_model(self.view.model)
86 |
87 | @property
88 | def codename(self):
89 | if not self.view.model:
90 | return self.view.permission_shortcode
91 | return '_'.join((
92 | self.view.permission_shortcode,
93 | self.view.model.__name__.lower(),
94 | ))
95 |
96 | def get_or_create_permission(self):
97 | return Permission.objects.update_or_create(
98 | content_type=self.content_type,
99 | codename=self.codename,
100 | defaults=dict(
101 | name=getattr(
102 | self.view,
103 | 'title_menu',
104 | self.view.permission_shortcode
105 | ),
106 | )
107 | )[0]
108 |
109 | @classmethod
110 | def factory(cls, view):
111 | kwargs = dict(
112 | pk=f'{view.__name__}',
113 | view=view,
114 | urlpath=view.urlpath,
115 | urlname=view.urlname,
116 | urlfullname=view.urlfullname,
117 | )
118 |
119 | if view.model:
120 | kwargs['model'] = view.model
121 |
122 | if view.router:
123 | kwargs['controller'] = Controller.factory(view.router)
124 | kwargs['fullurlpath'] = '/'.join((
125 | view.router.registry.urlpath,
126 | view.router.urlpath,
127 | view.urlpath,
128 | ))
129 |
130 | url = cls(**kwargs)
131 | return url
132 |
--------------------------------------------------------------------------------
/src/crudlfap/registry.py:
--------------------------------------------------------------------------------
1 | import collections
2 |
3 | from django.apps import apps
4 | from django.urls import path
5 |
6 | from .factory import Factory
7 | from .router import ViewsDescriptor
8 |
9 | crudlfap = apps.get_app_config('crudlfap') # pylint: disable=invalid-name
10 |
11 |
12 | class Registry(Factory, collections.OrderedDict):
13 | views = ViewsDescriptor()
14 |
15 | def get_menu(self, name, request, **kwargs):
16 | result = []
17 | for view in self.views:
18 | menus = getattr(view, 'menus', [])
19 | if name in menus and view(request=request).has_perm():
20 | result.append(view)
21 | for model, router in self.items():
22 | menu = router.get_menu(name, request, **kwargs)
23 | if not menu:
24 | continue
25 | result += menu
26 | return result
27 |
28 | def get_app_menus(self, name, request, **kwargs):
29 | """Sort Router instances by app name."""
30 | result = collections.OrderedDict()
31 | for model, router in self.items():
32 | menu = router.get_menu(name, request, **kwargs)
33 |
34 | if not menu:
35 | continue
36 |
37 | app = apps.get_app_config(model._meta.app_label)
38 | result.setdefault(app, [])
39 | result[app].append(router)
40 | return result
41 |
42 | def __getitem__(self, arg):
43 | """Return a router instance by model class, instance or dotted name."""
44 | from django.db import models
45 | if isinstance(arg, models.Model):
46 | arg = type(arg)
47 | elif isinstance(arg, str) and '.' in arg:
48 | arg = apps.get_model(*arg.split('.'))
49 | return super().__getitem__(arg)
50 |
51 | def __init__(self, views=None, *a, **attrs):
52 | self.views = views or []
53 | super().__init__(*a)
54 | for k, v in attrs.items():
55 | setattr(self, k, v)
56 |
57 | def get_urlpatterns(self):
58 | for view in self.views:
59 | view.registry = self
60 |
61 | return [
62 | router.urlpattern for router in self.values()
63 | ] + [view.urlpattern for view in self.views]
64 |
65 | def get_urlpattern(self):
66 | urlpath = self.urlpath
67 | if urlpath and not urlpath.endswith('/'):
68 | urlpath += '/'
69 |
70 | return path(urlpath, (
71 | self.urlpatterns,
72 | self.app_name,
73 | self.namespace,
74 | ))
75 |
76 | def get_app_name(self):
77 | return 'crudlfap'
78 |
79 | def get_namespace(self):
80 | return None
81 |
82 | def get_urlpath(self):
83 | return ''
84 |
85 | def get_title(self):
86 | return 'CRUDLFA+'
87 |
88 | def get_navbar_color(self):
89 | return ''
90 |
--------------------------------------------------------------------------------
/src/crudlfap/shortcuts.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa: F401
2 | """Import everything we expose in crudlfap namespace."""
3 |
4 | from .apps import _installed
5 | from .factory import Factory
6 | from .mixins import * # noqa
7 | from .registry import Registry
8 | from .route import Route
9 | from .router import Router, Views, ViewsDescriptor
10 | from .site import site
11 | from .views import * # noqa
12 |
--------------------------------------------------------------------------------
/src/crudlfap/site.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import gettext_lazy as _
2 |
3 | from .registry import Registry
4 | from .views.generic import TemplateView
5 | from .views.api import SchemaView
6 |
7 | site = Registry(
8 | views=[
9 | TemplateView.clone(
10 | icon='home',
11 | template_name='crudlfap/home.html',
12 | menus=['main'],
13 | title=_('Home'),
14 | title_menu=_('Home'),
15 | title_heading='',
16 | urlname='home',
17 | urlpath='',
18 | authenticate=False,
19 | ),
20 | SchemaView,
21 | TemplateView.clone(
22 | icon='api',
23 | template_name='crudlfap/api.html',
24 | menus=['main'],
25 | title=_('API'),
26 | title_menu=_('Api'),
27 | title_heading='',
28 | urlname='api',
29 | urlpath='api',
30 | authenticate=False,
31 | unpoly=False,
32 | ),
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/src/crudlfap/test_conf.py:
--------------------------------------------------------------------------------
1 | from .conf import install_optional
2 |
3 |
4 | def get_installed():
5 | return ['django.contrib.staticfiles', 'crudlfap', 'myapp'].copy()
6 |
7 |
8 | def test_install_optional():
9 | bs3 = {'debug_toolbar': {'after': 'django.contrib.staticfiles'}}
10 | messages = {
11 | 'django.contrib.messages': {
12 | 'before': 'django.contrib.staticfiles'
13 | }
14 | }
15 | nonexistent = {
16 | 'nonexistent': None,
17 | }
18 | ctx = {
19 | 'crudlfap_auth': None,
20 | }
21 | after = get_installed()
22 | install_optional([bs3], after)
23 | expect = [
24 | 'django.contrib.staticfiles',
25 | 'debug_toolbar',
26 | 'crudlfap',
27 | 'myapp',
28 | ]
29 | assert after == expect, "Failed to insert bootstrap3 after crudlfap"
30 | before_and_after = get_installed()
31 | install_optional([bs3, messages], before_and_after)
32 | expected = ['django.contrib.messages', 'django.contrib.staticfiles',
33 | 'debug_toolbar', 'crudlfap', 'myapp']
34 | assert before_and_after == expected, "Failed before and after"
35 | nono = get_installed()
36 | install_optional([nonexistent], nono)
37 | assert nono == get_installed(), "Non existing app installed"
38 | mod_attr = get_installed()
39 | install_optional([ctx], mod_attr)
40 | expected = ['django.contrib.staticfiles', 'crudlfap', 'myapp',
41 | 'crudlfap_auth']
42 | assert mod_attr == expected, "Failed to append module attribute reference"
43 |
--------------------------------------------------------------------------------
/src/crudlfap/test_factory.py:
--------------------------------------------------------------------------------
1 | from crudlfap_example.artist.models import Artist
2 |
3 | from .factory import Factory
4 |
5 |
6 | def test_factory():
7 | assert Factory.clone(foo=1).foo == 1
8 |
9 |
10 | def test_factory_with_model_property():
11 | class Router(Factory):
12 | model = Artist
13 |
14 | assert Router.clone(model=Artist).__name__ == 'ArtistRouter'
15 |
16 |
17 | def test_factory_with_model_argument():
18 | class Router(Factory):
19 | pass
20 |
21 | assert Router.clone(model=Artist).__name__ == 'ArtistRouter'
22 |
--------------------------------------------------------------------------------
/src/crudlfap/test_route.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django.views import generic
4 |
5 | from crudlfap_example.artist.models import Artist
6 |
7 | from .route import Route
8 | from .router import Router
9 |
10 |
11 | def test_model():
12 | route = Route.clone(model=Artist)
13 | assert route.model == Artist
14 |
15 |
16 | def test_model_router_fallback():
17 | route = Router(Artist, fields='__all__', views=[Route]).views[0]
18 | assert route.model == Artist
19 |
20 |
21 | def test_urlname():
22 | route = Route.clone(urlname='foo')
23 | assert route.urlname == 'foo'
24 |
25 |
26 | def test_urlname_from_name():
27 | class FooRoute(Route):
28 | pass
29 | assert FooRoute.urlname == 'foo'
30 |
31 |
32 | def test_urlpath():
33 | route = Route.clone(urlpath='foo')
34 | assert route.urlpath == 'foo'
35 |
36 |
37 | def test_urlpath_from_urlname():
38 | class FooRoute(Route):
39 | pass
40 | assert FooRoute.urlpath == 'foo'
41 |
42 |
43 | def test_urlpattern():
44 | class DetailView(generic.DetailView, Route):
45 | model = Artist
46 | p = DetailView.urlpattern
47 | assert p.name == 'detail'
48 | assert p.pattern.regex == re.compile('^detail\\Z')
49 | assert p.callback.view_class == DetailView
50 |
--------------------------------------------------------------------------------
/src/crudlfap/test_router.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.db import models
3 | from django.urls import resolve, reverse
4 | from django.views import generic
5 |
6 | from crudlfap import shortcuts as crudlfap
7 | from crudlfap_example.artist.models import Artist
8 |
9 | from .route import Route
10 | from .router import Router
11 |
12 |
13 | class TestRouter(Router):
14 | __test__ = False
15 | fields = '__all__'
16 |
17 |
18 | def test_views():
19 | router = TestRouter()
20 | view = crudlfap.TemplateView.clone(urlname='home')
21 | router.views = [view]
22 | assert router.views['home'] == view
23 | assert router.views[0].urlname == 'home'
24 |
25 | router.views['home'] = router.views['home'].clone(template_name='lol')
26 | assert router.views['home'].template_name == 'lol'
27 |
28 | del router.views['home']
29 | assert len(router.views) == 0
30 |
31 |
32 | def test_urlfield():
33 | assert Router().urlfield is None
34 |
35 |
36 | def test_urlfield_with_model():
37 | class TestModel(models.Model):
38 | pass
39 | assert Router(model=TestModel).urlfield == 'pk'
40 |
41 |
42 | def test_urlfield_with_slug():
43 | class TestModel(models.Model):
44 | slug = models.CharField(max_length=100)
45 | assert Router(model=TestModel).urlfield == 'slug'
46 |
47 |
48 | def test_namespace_none():
49 | assert Router().namespace is None
50 |
51 |
52 | def test_namespace():
53 | assert Router(namespace='lol').namespace == 'lol'
54 |
55 |
56 | def test_namespace_with_model():
57 | assert Router(model=Artist).namespace == 'artist'
58 |
59 |
60 | def test_path():
61 | assert Router(urlpath='foo').urlpath == 'foo'
62 |
63 |
64 | def test_path_with_model():
65 | assert Router(model=Artist).urlpath == 'artist'
66 |
67 |
68 | def test_app_name():
69 | assert Router(app_name='lol').app_name == 'lol'
70 |
71 |
72 | def test_app_name_with_model():
73 | assert Router(model=Artist).app_name == 'artist'
74 |
75 |
76 | def test_registry_default():
77 | assert Router().registry == crudlfap.site
78 |
79 |
80 | def test_registry():
81 | site = dict()
82 | assert Router(registry=site).registry == site
83 |
84 |
85 | class DetailView(Route, generic.DetailView):
86 | menus = ['object']
87 |
88 | # Not setting this would require
89 | # request.user.has_perm('artist.detail_artist', obj) to pass
90 | allowed = True
91 |
92 | # This is done by crudlfap generic ObjectView, but here tests django
93 | # generic views
94 | def get_urlargs(self):
95 | # This may be executed with just the class context (self.object
96 | # resolving to type(self).object, as from
97 | # Route.clone(object=something).url
98 | return [self.object.name]
99 |
100 |
101 | @pytest.fixture
102 | def router():
103 | return Router(model=Artist, views=[DetailView])
104 |
105 |
106 | def test_getitem(router):
107 | assert issubclass(router['detail'], DetailView)
108 |
109 |
110 | def test_urlpatterns(router):
111 | assert len(router.urlpatterns) == 1
112 | assert router.urlpatterns[0].name == 'detail'
113 |
114 |
115 | def test_urlpattern(router):
116 | assert reverse('detail', router) == '/detail'
117 | assert resolve('/detail', router).func.view_class == router.views[0]
118 |
119 |
120 | @pytest.mark.django_db
121 | def test_get_menu(router, srf):
122 | a = Artist(name='a')
123 | from crudlfap_auth.crudlfap import User
124 | srf.user = User.objects.create(is_superuser=True)
125 | req = srf.get('/')
126 | result = router.get_menu('object', req, object=a)
127 | assert len(result) == 1
128 | assert isinstance(result[0], DetailView)
129 | assert result[0].urlargs == ['a']
130 | assert type(result[0]).urlargs == ['a']
131 | assert str(result[0].url) == '/artist/a'
132 |
133 | b = type(result[0]).clone(object=a)
134 | assert str(b.url) == '/artist/a'
135 |
--------------------------------------------------------------------------------
/src/crudlfap/utils.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import FieldDoesNotExist
2 |
3 |
4 | def guess_urlfield(model):
5 | if not model:
6 | return None
7 |
8 | try:
9 | model._meta.get_field('slug')
10 | except FieldDoesNotExist:
11 | pass
12 | else:
13 | return 'slug'
14 |
15 | return 'pk'
16 |
--------------------------------------------------------------------------------
/src/crudlfap/views/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa: F401
2 |
3 | from .generic import (
4 | CreateView,
5 | DeleteView,
6 | DeleteObjectsView,
7 | DetailView,
8 | FormView,
9 | HistoryView,
10 | ListView,
11 | ModelFormView,
12 | ModelView,
13 | ObjectFormView,
14 | ObjectView,
15 | ObjectsFormView,
16 | ObjectsView,
17 | TemplateView,
18 | UpdateView,
19 | View,
20 | )
21 |
--------------------------------------------------------------------------------
/src/crudlfap/views/api.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django import http
4 | from crudlfap.views.generic import View
5 |
6 |
7 | schema = {
8 | 'basePath': '/v2',
9 | 'definitions': {},
10 | 'info': {},
11 | 'paths': {},
12 | 'schemes': ['https', 'http'],
13 | 'securityDefinitions': {},
14 | 'swagger': '2.0',
15 | 'tags': []
16 | }
17 |
18 |
19 | class SchemaView(View):
20 | authenticate = False
21 |
22 | def get(self, request, *args, **kwargs):
23 | schema['definitions'] = dict()
24 | schema['paths'] = dict()
25 | schema['host'] = request.get_host()
26 | for model, router in self.registry.items():
27 | for view in router.views:
28 | view = view.abstract(request=request)
29 | if not view.has_perm():
30 | continue
31 | if getattr(view, 'router', None):
32 | self.add_router_schema(request, view.router, schema)
33 | try:
34 | path_definition = view.swagger_path_definition
35 | except AttributeError:
36 | pass
37 | else:
38 | if path_definition:
39 | self.add_path_definition(path_definition, view, schema)
40 | return http.JsonResponse(schema)
41 |
42 | def add_path_definition(self, path_definition, view, schema):
43 | url = view.urlpath
44 | if router := getattr(view, 'router', None):
45 | if url:
46 | url = os.path.join(router.urlpath, url)
47 | else:
48 | url = router.urlpath
49 | if self.registry.urlpath:
50 | url = self.registry.urlpath + url
51 | url = '/' + url.replace('<', '{').replace('>', '}')
52 | schema['paths'][url] = path_definition
53 |
54 | def add_router_schema(self, request, router, schema):
55 | try:
56 | model_name = router.get_swagger_model_name(request)
57 | except AttributeError:
58 | pass
59 | else:
60 | if model_name and model_name not in schema['definitions']:
61 | model_definition = router.get_swagger_model_definition(request)
62 | if model_definition:
63 | schema['definitions'][model_name] = model_definition
64 |
--------------------------------------------------------------------------------
/src/crudlfap/views/generic.py:
--------------------------------------------------------------------------------
1 | """
2 | Crudlfa+ generic views and mixins.
3 |
4 | Crudlfa+ takes views further than Django and are expected to:
5 |
6 | - generate their URL definitions and reversions,
7 | - check if a user has permission for an object,
8 | - declare the names of the navigation menus they belong to.
9 | """
10 | from django import http
11 | from django.conf import settings
12 | from django.views import generic
13 |
14 | from .. import mixins
15 | from ..route import Route
16 |
17 | if 'django.contrib.admin' in settings.INSTALLED_APPS:
18 | from django.contrib.admin.models import LogEntry
19 | else:
20 | LogEntry = None
21 |
22 |
23 | class View(mixins.LockMixin, mixins.MenuMixin, Route, generic.View):
24 | """Base view for CRUDLFA+."""
25 |
26 |
27 | class TemplateView(mixins.TemplateMixin, View):
28 | """TemplateView for CRUDLFA+."""
29 |
30 |
31 | class ModelView(mixins.ModelMixin, TemplateView):
32 | pass
33 |
34 |
35 | class ObjectFormView(mixins.ObjectFormMixin, TemplateView):
36 | """Custom form view on an object."""
37 |
38 |
39 | class ObjectView(mixins.ObjectMixin, TemplateView):
40 | pass
41 |
42 |
43 | class ObjectsFormView(mixins.ObjectsFormMixin, TemplateView):
44 | pass
45 |
46 |
47 | class ObjectsView(mixins.ObjectsMixin, TemplateView):
48 | pass
49 |
50 |
51 | class FormView(mixins.FormMixin, TemplateView):
52 | """Base FormView class."""
53 |
54 | style = 'warning'
55 | default_template_name = 'crudlfap/form.html'
56 |
57 |
58 | class ModelFormView(mixins.ModelFormMixin, FormView):
59 | pass
60 |
61 |
62 | class CreateView(mixins.CreateMixin, ModelFormView):
63 | """View to create a model object."""
64 |
65 |
66 | class DeleteView(mixins.DeleteMixin, ObjectFormView):
67 | """View to delete an object."""
68 |
69 |
70 | class DeleteObjectsView(mixins.DeleteMixin, ObjectsFormView):
71 | """Delete selected objects."""
72 |
73 |
74 | class DetailView(mixins.DetailMixin, ObjectView):
75 | """Templated model object detail view which takes a field option."""
76 |
77 | def get_swagger_summary(self):
78 | return f'{self.model.__name__} Detail'
79 |
80 | def get_swagger_get(self):
81 | result = {
82 | 'operationId': f'find{self.model.__name__}ByStatus',
83 | 'produces': ['application/json'],
84 | 'responses': {
85 | '200': {
86 | 'description': 'successful operation',
87 | 'schema': {
88 | 'items': {
89 | '$ref': f'#/definitions/{self.model.__name__}'
90 | },
91 | 'type': 'array'
92 | }
93 | },
94 | '404': {
95 | 'description': 'Not found'
96 | },
97 | },
98 | 'summary': self.swagger_summary,
99 | 'tags': self.swagger_tags,
100 | }
101 | return result
102 |
103 |
104 | class HistoryView(mixins.ObjectMixin, generic.DetailView):
105 | pass
106 |
107 |
108 | class ListView(mixins.ListMixin, mixins.SearchMixin, mixins.FilterMixin,
109 | mixins.TableMixin, mixins.ObjectsMixin, TemplateView):
110 |
111 | def get_object_list(self):
112 | if self.filterset:
113 | self.object_list = self.filterset.qs
114 | else:
115 | self.object_list = self.queryset
116 |
117 | if self.search_form:
118 | self.object_list = self.search_filter(self.object_list)
119 |
120 | return self.object_list
121 |
122 | def get_listactions(self):
123 | return self.router.get_menu('list_action', self.request)
124 |
125 | def get_swagger_get(self):
126 | parameters = []
127 | if self.filterset:
128 | for name, field in self.filterset.form.fields.items():
129 | parameters.append({
130 | 'collectionFormat': 'single',
131 | 'description': field.help_text,
132 | 'in': 'query',
133 | 'name': name,
134 | 'required': field.required,
135 | 'type': 'string'
136 | })
137 |
138 | return {
139 | # 'description': 'Multiple status are comma separated',
140 | 'operationId': f'find{self.model.__name__}ByStatus',
141 | 'parameters': parameters,
142 | 'produces': ['application/json'],
143 | 'responses': {
144 | '200': {
145 | 'description': 'successful operation',
146 | 'schema': {
147 | 'items': {
148 | '$ref': f'#/definitions/{self.model.__name__}'
149 | },
150 | 'type': 'array'
151 | }
152 | },
153 | '400': {'description': 'Invalid parameter'}
154 | },
155 | 'summary': self.title,
156 | 'tags': self.swagger_tags,
157 | }
158 |
159 | def get_json_fields(self):
160 | return self.router.json_fields
161 |
162 | def serialize(self, obj):
163 | if self.router:
164 | return self.router.serialize(obj, self.json_fields)
165 | return {
166 | field: getattr(
167 | self,
168 | f'get_{field}_json',
169 | 'get_FIELD_json',
170 | )(obj, field)
171 | for field in self.json_fields
172 | }
173 |
174 | def json_get(self, request, *args, **kwargs):
175 | rows = []
176 | for row in self.table.paginated_rows:
177 | rows.append(self.serialize(row.record))
178 | data = dict(
179 | results=rows,
180 | paginator=dict(
181 | page_number=self.table.page.number,
182 | per_page=self.table.page.paginator.per_page,
183 | total_pages=self.table.page.paginator.num_pages,
184 | total_objects=self.table.page.paginator.count,
185 | has_next=self.table.page.has_next(),
186 | has_previous=self.table.page.has_previous(),
187 | )
188 | )
189 | return http.JsonResponse(data)
190 |
191 |
192 | class UpdateView(mixins.UpdateMixin, ObjectFormView):
193 | """Model update view."""
194 |
--------------------------------------------------------------------------------
/src/crudlfap_auth/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_auth/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_auth/backends.py:
--------------------------------------------------------------------------------
1 | class ViewBackend:
2 | def authenticate(self, *args):
3 | """
4 | Always return ``None`` to prevent authentication within this backend.
5 | """
6 | return None
7 |
8 | def has_perm(self, user_obj, perm, obj=None): # noqa
9 | try:
10 | if not obj.authenticate:
11 | return True
12 | except AttributeError:
13 | return False
14 |
15 | if not user_obj.is_authenticated:
16 | return False
17 |
18 | if user_obj.is_superuser:
19 | return True
20 |
21 | try:
22 | if obj.allowed_groups == 'any':
23 | return True
24 | except AttributeError:
25 | return False
26 |
27 | for group in user_obj.groups.all():
28 | if group.name in obj.allowed_groups:
29 | return True
30 |
31 | return False
32 |
--------------------------------------------------------------------------------
/src/crudlfap_auth/crudlfap.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.conf import settings
3 | from django.contrib.auth.models import Group
4 | from django.urls import reverse
5 |
6 | from crudlfap import shortcuts as crudlfap
7 | from crudlfap import html
8 |
9 | from . import views
10 |
11 | User = apps.get_model(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'))
12 |
13 |
14 | def login_logout(request, menu):
15 | if request.user.is_authenticated:
16 | menu.append(html.MDCListItem(
17 | 'Logout',
18 | icon='logout',
19 | href=reverse('logout'),
20 | style='text-decoration: none',
21 | up_target='body',
22 | up_method='POST',
23 | up_follow=True,
24 | tag='a',
25 | ))
26 | else:
27 | menu.insert(1, html.MDCListItem(
28 | 'Login',
29 | icon='login',
30 | href=reverse('login'),
31 | style='text-decoration: none',
32 | up_target=html.UNPOLY_TARGET_ALL,
33 | tag='a',
34 | ))
35 | return menu
36 |
37 |
38 | html.mdcDrawer.menu_hooks.append(login_logout)
39 |
40 |
41 | crudlfap.Router(
42 | User,
43 | views=[
44 | crudlfap.DeleteObjectsView,
45 | crudlfap.DeleteView,
46 | crudlfap.UpdateView.clone(
47 | fields=[
48 | 'username',
49 | 'email',
50 | 'first_name',
51 | 'last_name',
52 | 'groups',
53 | 'is_superuser',
54 | 'is_staff',
55 | 'is_active',
56 | ]
57 | ),
58 | crudlfap.CreateView.clone(
59 | fields=[
60 | 'username',
61 | 'email',
62 | 'groups',
63 | 'is_staff',
64 | 'is_superuser'
65 | ],
66 | ),
67 | views.PasswordView,
68 | views.BecomeUser,
69 | crudlfap.DetailView.clone(exclude=['password']),
70 | crudlfap.ListView.clone(
71 | search_fields=[
72 | 'username',
73 | 'email',
74 | 'first_name',
75 | 'last_name'
76 | ],
77 | table_fields=[
78 | 'username',
79 | 'email',
80 | 'first_name',
81 | 'last_name',
82 | 'is_staff',
83 | 'is_superuser'
84 | ],
85 | filter_fields=[
86 | 'groups',
87 | 'is_superuser',
88 | 'is_staff'
89 | ],
90 | ),
91 | ],
92 | urlfield='username',
93 | icon='person',
94 | ).register()
95 |
96 |
97 | class GroupUpdateView(crudlfap.UpdateView):
98 | def get_form_class(self):
99 | cls = super().get_form_class()
100 | cls.base_fields['permissions'].queryset = (
101 | cls.base_fields['permissions'].queryset.select_related(
102 | 'content_type'))
103 | return cls
104 |
105 |
106 | crudlfap.Router(
107 | Group,
108 | fields=['name', 'permissions'],
109 | icon='group',
110 | views=[
111 | crudlfap.DeleteObjectsView,
112 | crudlfap.DeleteView,
113 | GroupUpdateView,
114 | crudlfap.CreateView,
115 | crudlfap.DetailView,
116 | crudlfap.ListView,
117 | ],
118 | ).register()
119 |
120 | crudlfap.site.views.append(views.Become.clone(model=User))
121 |
--------------------------------------------------------------------------------
/src/crudlfap_auth/html.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.urls import reverse
3 | from django.contrib.sites.models import Site
4 | from crudlfap.html import * # noqa
5 |
6 | User = apps.get_model(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'))
7 |
8 |
9 | @template('registration/login.html', App)
10 | class LoginFormViewComponent(FormContainer):
11 | demo = False
12 |
13 | def context(self, *content, **context):
14 | context['view'].title = 'Login'
15 | return super().context(*content, **context)
16 |
17 | def to_html(self, *content, view, form, **kwargs):
18 | site = Site.objects.get_current(view.request)
19 | content = content or [
20 | H2('Welcome to ' + site.name, style='text-align: center;'),
21 | ]
22 | return super().to_html(
23 | Form(
24 | *content,
25 | # OAuthConnect(),
26 | # Span('Or enter email and password:', cls='center-text'),
27 | CSRFInput(view.request),
28 | form,
29 | Div(
30 | MDCButtonRaised('Continue'),
31 | A(
32 | MDCButtonOutlined('forgot password'),
33 | href=reverse('password_reset')
34 | ),
35 | style='display: flex; justify-content: space-between',
36 | ),
37 | method='POST',
38 | action=view.request.path_info,
39 | cls='form card',
40 | up_target=None,
41 | up_layer=None,
42 | ),
43 | cls='',
44 | )
45 |
46 |
47 | @template('registration/logged_out.html', App)
48 | class LogoutViewComponent(FormContainer):
49 | def context(self, *content, **context):
50 | context['view'].title = 'Logout'
51 | return super().context(*content, **context)
52 |
53 | def __init__(self, *content, **context):
54 | super().__init__(
55 | H4('You have been logged out'),
56 | Section(
57 | 'Thank you for spending time on our site today.',
58 | ),
59 | Div(
60 | MDCButton('Login again', tag='a', href=reverse('login')),
61 | style='display:flex; justify-content: flex-end;',
62 | ),
63 | cls='card',
64 | style='text-align: center',
65 | )
66 |
67 |
68 | @template('registration/password_reset_form.html', App)
69 | class PasswordResetCard(FormContainer):
70 | def to_html(self, *content, view, form, **context):
71 | return super().to_html(
72 | H4('Reset your password', style='text-align: center;'),
73 | Form(
74 | CSRFInput(view.request),
75 | form,
76 | MDCButtonRaised('Reset password'),
77 | method='POST',
78 | cls='form',
79 | ),
80 | cls='card',
81 | )
82 |
83 |
84 | @template('registration/password_reset_confirm.html', App)
85 | class PasswordResetConfirm(FormContainer):
86 | def context(self, *content, **context):
87 | context['view'].title = 'Password reset confirm'
88 | return super().context(*content, **context)
89 |
90 | def to_html(self, *content, view, form, **context):
91 | return super().to_html(
92 | H4('Reset your password', style='text-align: center;'),
93 | Form(
94 | CSRFInput(view.request),
95 | form,
96 | MDCButtonRaised('confirm'),
97 | method='POST',
98 | cls='form',
99 | ),
100 | cls='card',
101 | )
102 |
103 |
104 | @template('registration/password_reset_complete.html', App)
105 | class PasswordResetComplete(FormContainer):
106 | def __init__(self, **context):
107 | super().__init__(
108 | H4('Your password have been reset', cls='center-text'),
109 | Div(
110 | 'You may go ahead and ',
111 | A('log in', href=reverse('login')),
112 | ' now',
113 | ),
114 | )
115 |
116 |
117 | @template('registration/password_reset_done.html', App)
118 | class PasswordResetDone(FormContainer):
119 | def __init__(self, **context):
120 | super().__init__(
121 | H4('A link has been sent to your email address'),
122 | A('Go to login page', href=reverse('login')),
123 | cls='card',
124 | style='text-align: center;',
125 | )
126 |
--------------------------------------------------------------------------------
/src/crudlfap_auth/locale/fr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2018-11-14 12:11+0100\n"
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13 | "Last-Translator: FULL NAME \n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: \n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n"
20 |
21 | #: src/crudlfap_auth/views.py:33
22 | msgid "update"
23 | msgstr ""
24 |
25 | #: src/crudlfap_auth/views.py:57
26 | msgid "become"
27 | msgstr "devenir"
28 |
29 | #: src/crudlfap_auth/views.py:82
30 | msgid "Switched to user %s"
31 | msgstr "Vous usurpez maintenant: %s"
32 |
33 | #: src/crudlfap_auth/views.py:110
34 | msgid "Switched back to your user %s"
35 | msgstr "De retour sur votre utilisateur %s"
36 |
37 | #: src/crudlfap_auth/views.py:117
38 | msgid "You are still superuser %s"
39 | msgstr "Vous êtes toujours le superutilisateur %s"
40 |
--------------------------------------------------------------------------------
/src/crudlfap_auth/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_auth/models.py
--------------------------------------------------------------------------------
/src/crudlfap_auth/views.py:
--------------------------------------------------------------------------------
1 | """
2 | Crudlfa+ PasswordView, Become and BecomeUser views.
3 |
4 | Crudlfa+ takes views further than Django and are expected to:
5 |
6 | - generate their URL definitions and reversions,
7 | - check if a user has permission for an object,
8 | - declare the names of the navigation menus they belong to.
9 | """
10 |
11 | import logging
12 |
13 | from django import http
14 | from django.contrib import auth, messages
15 | from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
16 | from django.utils.translation import gettext_lazy as _
17 |
18 | from crudlfap import shortcuts as crudlfap
19 |
20 | logger = logging.getLogger()
21 |
22 |
23 | class PasswordView(crudlfap.UpdateView):
24 | slug = 'password'
25 | icon = 'vpn_key'
26 | color = 'purple'
27 | controller = 'modal'
28 | action = 'click->modal#open'
29 |
30 | def get_title_submit(self):
31 | return _('update').capitalize()
32 |
33 | def get_form_class(self):
34 | if self.object == self.request.user:
35 | cls = PasswordChangeForm
36 | else:
37 | cls = SetPasswordForm
38 | # This fixes the form messages feature from UpdateView
39 | return type(cls.__name__, (cls,), dict(instance=self.object))
40 |
41 | def get_form_kwargs(self):
42 | kwargs = super().get_form_kwargs()
43 | kwargs['user'] = self.object
44 | return kwargs
45 |
46 |
47 | class BecomeUser(crudlfap.ObjectView):
48 | urlname = 'su'
49 | menus = ['object']
50 | icon = 'accessibility_new'
51 | color = 'purple'
52 | link_attributes = {'data-noprefetch': 'true', 'up-target': 'body'}
53 |
54 | def become(self):
55 | """
56 | Implement this method to override the default become logic.
57 | """
58 | auth.login(self.request, self.object, backend=self.backend)
59 |
60 | def get_title_menu(self):
61 | return _('become').capitalize()
62 |
63 | def get_object(self, queryset=None):
64 | user = super().get_object()
65 |
66 | if user:
67 | user.backend = self.backend
68 | else:
69 | messages.error(
70 | self.request,
71 | 'Could not find user {}'.format(self.kwargs['username'])
72 | )
73 |
74 | return user
75 |
76 | def get(self, request, *a, **k):
77 | logger.info('BecomeUser by {}'.format(self.request.user))
78 | request_user = request.user
79 | self.request = request
80 | result = self.become()
81 | if 'become_user' not in request.session:
82 | request.session['become_user_realname'] = str(request_user)
83 | request.session['become_user'] = str(request_user.pk)
84 | messages.info(
85 | request,
86 | _('Switched to user %s') % request.user
87 | )
88 | return result if result else http.HttpResponseRedirect(
89 | '/' + self.router.registry.urlpath)
90 |
91 | def get_backend(self):
92 | return 'django.contrib.auth.backends.ModelBackend'
93 |
94 |
95 | class Become(crudlfap.View):
96 | urlname = 'su'
97 |
98 | def has_perm(self):
99 | return 'become_user' in self.request.session
100 |
101 | def get_backend(self):
102 | return 'django.contrib.auth.backends.ModelBackend'
103 |
104 | def get_object(self):
105 | user = self.model.objects.get(pk=self.request.session['become_user'])
106 | user.backend = self.backend
107 | return user
108 |
109 | def get(self, request, *a, **k):
110 | logger.info('Become by {}'.format(self.request.user))
111 | if 'become_user' not in request.session:
112 | logger.debug(
113 | 'No become_user in session {}'.format(self.request.user))
114 | return http.HttpResponseNotFound()
115 |
116 | user = self.get_object()
117 | auth.login(request, user)
118 | messages.info(
119 | request,
120 | _('Switched back to your user %s') % user
121 | )
122 | return http.HttpResponseRedirect('/' + self.registry.urlpath)
123 |
--------------------------------------------------------------------------------
/src/crudlfap_example/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_example/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_example/artist/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_example/artist/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_example/artist/crudlfap.py:
--------------------------------------------------------------------------------
1 | from crudlfap import shortcuts as crudlfap
2 |
3 | from .models import Artist
4 |
5 | crudlfap.Router(
6 | Artist,
7 | fields='__all__',
8 | icon='record_voice_over',
9 | ).register()
10 |
--------------------------------------------------------------------------------
/src/crudlfap_example/artist/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.10 on 2017-09-17 22:09
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Artist',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('name', models.CharField(max_length=100)),
21 | ],
22 | options={
23 | 'ordering': ('name',),
24 | },
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/src/crudlfap_example/artist/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_example/artist/migrations/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_example/artist/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Artist(models.Model):
5 | name = models.CharField(max_length=100)
6 |
7 | class Meta:
8 | ordering = ('name',)
9 |
10 | def __str__(self):
11 | return self.name
12 |
--------------------------------------------------------------------------------
/src/crudlfap_example/artist/test_api.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pytest
3 |
4 | from crudlfap_auth.crudlfap import User
5 |
6 |
7 | @pytest.mark.django_db
8 | def test_api(client):
9 | user = User.objects.create(is_superuser=True, is_active=True)
10 | client.force_login(user)
11 |
12 | # test valid form
13 | response = client.post(
14 | '/artist/create',
15 | json.dumps(dict(
16 | name='test artist',
17 | )),
18 | content_type='application/json'
19 | )
20 | assert response.status_code == 201
21 |
22 | # test invalid form
23 | response = client.post(
24 | '/artist/create',
25 | json.dumps(dict(
26 | name='',
27 | )),
28 | content_type='application/json'
29 | )
30 | assert response.status_code == 405
31 |
32 | # test list get
33 | response = client.get(
34 | '/artist',
35 | HTTP_ACCEPT='application/json',
36 | )
37 | assert response.status_code == 200
38 | data = response.json()
39 | assert len(data['results']) == 1
40 | assert data['results'][0]['name'] == 'test artist'
41 | response = client.get(
42 | f'/artist/{data["results"][0]["id"]}',
43 | HTTP_ACCEPT='application/json'
44 | )
45 | assert response.json() == {'id': 1, 'name': 'test artist'}
46 |
--------------------------------------------------------------------------------
/src/crudlfap_example/blog/crudlfap.py:
--------------------------------------------------------------------------------
1 | from crudlfap import shortcuts as crudlfap
2 |
3 | from .models import Post
4 |
5 |
6 | class AuthBackend:
7 | def authenticate(self, *args):
8 | return None # prevent auth from this backend
9 |
10 | def has_perm(self, user_obj, perm, obj=None):
11 | view = obj
12 |
13 | if view.model != Post:
14 | return False
15 |
16 | user = user_obj
17 | code = view.permission_shortcode
18 |
19 | if code in ('list', 'detail'):
20 | return True
21 | elif code == 'add':
22 | return user.is_authenticated
23 | elif code == 'change':
24 | return view.object.editable(user)
25 | elif code == 'delete':
26 | if hasattr(view, 'object'):
27 | return view.object.editable(user)
28 |
29 | # DeleteObjects relies on get_queryset to secure runtime
30 | return user.is_authenticated
31 |
32 | return super().has_perm(user_obj, perm, obj)
33 |
34 |
35 | class PostMixin:
36 | def get_exclude(self):
37 | if not self.request.user.is_staff:
38 | return ['owner']
39 | return super().get_exclude()
40 |
41 |
42 | class PostCreateView(PostMixin, crudlfap.CreateView):
43 | def form_valid(self):
44 | self.form.instance.owner = self.request.user
45 | return super().form_valid()
46 |
47 |
48 | class PostUpdateView(PostMixin, crudlfap.UpdateView):
49 | pass
50 |
51 |
52 | class PostListView(crudlfap.ListView):
53 | def get_filter_fields(self):
54 | if self.request.user.is_staff:
55 | return ['owner']
56 | return []
57 |
58 |
59 | class PostRouter(crudlfap.Router):
60 | fields = '__all__'
61 | icon = 'book'
62 | model = Post
63 |
64 | views = [
65 | crudlfap.DeleteObjectsView,
66 | crudlfap.DeleteView,
67 | PostUpdateView,
68 | PostCreateView,
69 | crudlfap.DetailView,
70 | PostListView.clone(
71 | search_fields=['name'],
72 | ),
73 | ]
74 |
75 | def get_queryset(self, view):
76 | qs = self.model.objects.get_queryset()
77 | if view.request.user.is_superuser:
78 | return qs
79 | elif view.permission_shortcode in ('change', 'delete'):
80 | return qs.editable(view.request.user)
81 | elif view.permission_shortcode in ('list', 'detail'):
82 | return qs.readable(view.request.user)
83 | return qs.none()
84 |
85 |
86 | PostRouter().register()
87 |
--------------------------------------------------------------------------------
/src/crudlfap_example/blog/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-04-15 23:59
2 |
3 | import django.db.models.deletion
4 | import django.utils.timezone
5 | from django.conf import settings
6 | from django.db import migrations, models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Post',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('name', models.CharField(max_length=100, verbose_name='title')),
23 | ('publish', models.DateTimeField(default=django.utils.timezone.now)),
24 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
25 | ],
26 | options={
27 | 'ordering': ('name',),
28 | },
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/src/crudlfap_example/blog/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_example/blog/migrations/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_example/blog/models.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db import models
3 | from django.utils import timezone
4 |
5 |
6 | class PostQuerySet(models.QuerySet):
7 | def readable(self, user):
8 | if user.is_staff or user.is_superuser:
9 | return self
10 |
11 | published = models.Q(publish__lte=timezone.now())
12 |
13 | if not user.is_authenticated:
14 | return self.filter(published)
15 |
16 | return self.filter(models.Q(owner=user) | published)
17 |
18 | def editable(self, user):
19 | if not user.is_authenticated:
20 | return self.none()
21 |
22 | if user.is_staff or user.is_superuser:
23 | return self
24 |
25 | return self.filter(owner=user)
26 |
27 |
28 | class PostManager(models.Manager):
29 | def get_queryset(self):
30 | return PostQuerySet(self.model, using=self._db)
31 |
32 |
33 | class Post(models.Model):
34 | name = models.CharField(max_length=100, verbose_name='title')
35 | publish = models.DateTimeField(default=timezone.now)
36 | owner = models.ForeignKey(
37 | getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
38 | on_delete=models.CASCADE,
39 | )
40 |
41 | objects = PostManager()
42 |
43 | class Meta:
44 | ordering = ('name',)
45 |
46 | def __str__(self):
47 | return self.name
48 |
49 | def editable(self, user):
50 | return user.is_staff or user.is_superuser or user.pk == self.owner_id
51 |
--------------------------------------------------------------------------------
/src/crudlfap_example/blog/test_security.py:
--------------------------------------------------------------------------------
1 | """
2 | Test security with crudlfap_example.song setup.
3 |
4 | It's a bit paranoid, but i'll sleep better with this: i don't trust users, i
5 | don't trust myself either :)
6 | """
7 | from datetime import timedelta
8 |
9 | import pytest
10 | from django.utils import timezone
11 |
12 | from crudlfap import shortcuts as crudlfap
13 | from crudlfap_auth.crudlfap import User
14 | from crudlfap_example.blog.models import Post
15 |
16 |
17 | def user(**attrs):
18 | user, created = User.objects.get_or_create(**attrs)
19 | if created:
20 | user.set_password('password')
21 | user.save()
22 | return user
23 |
24 |
25 | def user_client(client, **userattrs):
26 | if not userattrs.get('username', None):
27 | return client
28 | user(**userattrs)
29 | if not client.login(username=userattrs['username'], password='password'):
30 | raise Exception('Could not auth as ' + userattrs['username'])
31 | return client
32 |
33 |
34 | router = pytest.fixture(lambda: crudlfap.site[Post])
35 | now = timezone.now()
36 | yesterday = now - timedelta(days=1)
37 | tomorrow = now + timedelta(days=1)
38 |
39 | user0 = pytest.fixture(lambda: user(username='user0'))
40 | user1 = pytest.fixture(lambda: user(username='user1'))
41 | staff = pytest.fixture(lambda: user(username='staff', is_staff=True))
42 |
43 | post0 = pytest.fixture(lambda user0: Post.objects.get_or_create(
44 | owner=user0, name='post0', publish=yesterday)[0])
45 | post1 = pytest.fixture(lambda user0: Post.objects.get_or_create(
46 | owner=user0, name='post1', publish=tomorrow)[0])
47 | post2 = pytest.fixture(lambda user1: Post.objects.get_or_create(
48 | owner=user1, name='post2', publish=yesterday)[0])
49 |
50 |
51 | def url(name, obj):
52 | return crudlfap.site[Post][name].clone(object=obj).url
53 |
54 |
55 | @pytest.mark.django_db
56 | def test_list_view(router, client, user0, user1, post0, post1, post2):
57 | list_url = router['list'].url
58 | res = user_client(client, username='user0').get(list_url)
59 | assert b'post0' in res.content
60 | assert b'post1' in res.content
61 | assert b'post2' in res.content
62 | for name in ('update', 'delete'):
63 | view = router[name]
64 | assert str(view.clone(object=post0).url) in str(res.content)
65 | assert str(view.clone(object=post1).url) in str(res.content)
66 | assert str(view.clone(object=post2).url) not in str(res.content)
67 |
68 | res = user_client(client, username='user1').get(list_url)
69 | assert b'post0' in res.content
70 | assert b'post1' not in res.content
71 | assert b'post2' in res.content
72 | for name in ('update', 'delete'):
73 | view = router[name]
74 | assert str(view.clone(object=post0).url) not in str(res.content)
75 | assert str(view.clone(object=post1).url) not in str(res.content)
76 | assert str(view.clone(object=post2).url) in str(res.content)
77 |
78 |
79 | @pytest.mark.django_db
80 | def test_anonymous_detail_published(router, client, post0, post1):
81 | assert client.get(url('detail', post0)).status_code == 200
82 | assert client.get(url('detail', post1)).status_code == 404
83 |
84 |
85 | @pytest.mark.django_db
86 | def test_owner_detail_unpublished(router, client, post0, post1):
87 | client = user_client(client, username='user0')
88 | assert client.get(url('detail', post0)).status_code == 200
89 | assert client.get(url('detail', post1)).status_code == 200
90 |
91 |
92 | @pytest.mark.django_db
93 | def test_nonowner_detail_unpublished(router, client, post0, post1):
94 | client = user_client(client, username='user1')
95 | assert client.get(url('detail', post0)).status_code == 200
96 | assert client.get(url('detail', post1)).status_code == 404
97 |
98 |
99 | @pytest.mark.parametrize('name', ['delete', 'update'])
100 | @pytest.mark.django_db
101 | def test_anonymous_edit_published(router, client, post0, post1, name):
102 | assert client.get(url(name, post0)).status_code == 404
103 | assert client.get(url(name, post1)).status_code == 404
104 |
105 |
106 | @pytest.mark.parametrize('name', ['delete', 'update'])
107 | @pytest.mark.django_db
108 | def test_owner_edits(router, client, post0, post1, post2, name):
109 | client = user_client(client, username='user0')
110 | assert client.get(url(name, post0)).status_code == 200
111 | assert client.get(url(name, post1)).status_code == 200
112 | assert client.get(url(name, post2)).status_code == 404
113 |
114 | client = user_client(client, username='user1')
115 | assert client.get(url(name, post0)).status_code == 404
116 | assert client.get(url(name, post1)).status_code == 404
117 | assert client.get(url(name, post2)).status_code == 200
118 |
--------------------------------------------------------------------------------
/src/crudlfap_example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from crudlfap.manage import main
3 |
4 | if __name__ == '__main__':
5 | main('crudlfap_example.settings')
6 |
--------------------------------------------------------------------------------
/src/crudlfap_example/settings.py:
--------------------------------------------------------------------------------
1 | from crudlfap.settings import * # noqa
2 |
3 | BASE_DIR = Path(__file__).resolve().parent.parent
4 |
5 | INSTALLED_APPS += [ # noqa
6 | # CRUDLFA+ extras
7 | 'django_registration',
8 | 'crudlfap_registration',
9 |
10 | # CRUDLFA+ examples
11 | 'crudlfap_example.artist',
12 | 'crudlfap_example.song',
13 | 'crudlfap_example.blog',
14 | ]
15 |
16 | ROOT_URLCONF = os.path.dirname(__file__).split('/')[-1] + '.urls'
17 |
18 | DATABASES = {
19 | 'default': {
20 | 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.sqlite3'),
21 | 'NAME': os.getenv('DB_NAME', os.path.join(
22 | os.path.dirname(__file__),
23 | '..',
24 | 'db.sqlite3',
25 | )),
26 | 'HOST': os.getenv('DB_HOST'),
27 | 'USER': os.getenv('DB_USER'),
28 | 'PASSWORD': os.getenv('DB_PASS'),
29 | }
30 | }
31 |
32 | install_optional(OPTIONAL_APPS, INSTALLED_APPS) # noqa
33 | install_optional(OPTIONAL_MIDDLEWARE, MIDDLEWARE) # noqa
34 |
35 | AUTHENTICATION_BACKENDS += [ # noqa
36 | 'crudlfap_example.blog.crudlfap.AuthBackend',
37 | ]
38 |
39 | LOGGING = {
40 | 'version': 1,
41 | 'disable_existing_loggers': False,
42 | 'formatters': {
43 | 'verbose': {
44 | 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
45 | },
46 | },
47 | 'handlers': {
48 | 'console': {
49 | 'level': 'DEBUG',
50 | 'class': 'logging.StreamHandler',
51 | 'stream': sys.stdout,
52 | 'formatter': 'verbose'
53 | },
54 | },
55 | 'loggers': {
56 | '': {
57 | 'handlers': ['console'],
58 | 'level': 'DEBUG',
59 | 'propagate': True,
60 | },
61 | },
62 | }
63 |
--------------------------------------------------------------------------------
/src/crudlfap_example/song/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_example/song/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_example/song/crudlfap.py:
--------------------------------------------------------------------------------
1 | from crudlfap import shortcuts as crudlfap
2 |
3 | from .models import Song
4 |
5 |
6 | class SongMixin:
7 | allowed_groups = 'any'
8 |
9 | def get_exclude(self):
10 | if not self.request.user.is_staff:
11 | return ['owner']
12 | return super().get_exclude()
13 |
14 |
15 | class SongCreateView(SongMixin, crudlfap.CreateView):
16 | def form_valid(self):
17 | self.form.instance.owner = self.request.user
18 | return super().form_valid()
19 |
20 |
21 | class SongRouter(crudlfap.Router):
22 | fields = '__all__'
23 | icon = 'album'
24 | model = Song
25 |
26 | views = [
27 | crudlfap.DeleteView.clone(SongMixin),
28 | crudlfap.UpdateView.clone(SongMixin),
29 | SongCreateView,
30 | crudlfap.DetailView.clone(
31 | authenticate=False,
32 | ),
33 | crudlfap.ListView.clone(
34 | authenticate=False,
35 | filter_fields=['artist'],
36 | search_fields=['artist__name', 'name'],
37 | ),
38 | ]
39 |
40 | def get_queryset(self, view):
41 | user = view.request.user
42 |
43 | if user.is_staff or user.is_superuser:
44 | return self.model.objects.all()
45 | elif not user.is_authenticated:
46 | return self.model.objects.none()
47 |
48 | return self.model.objects.filter(owner=user)
49 |
50 |
51 | SongRouter().register()
52 |
--------------------------------------------------------------------------------
/src/crudlfap_example/song/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.3 on 2018-04-15 15:22
2 |
3 | import django.db.models.deletion
4 | from django.conf import settings
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ('artist', '0001_initial'),
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Song',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('name', models.CharField(max_length=100, verbose_name='title')),
23 | ('duration', models.IntegerField(default=320)),
24 | ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')),
25 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
26 | ],
27 | options={
28 | 'ordering': ('name',),
29 | },
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/src/crudlfap_example/song/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_example/song/migrations/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_example/song/models.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db import models
3 | from django.utils.translation import gettext_lazy as _
4 |
5 |
6 | class Song(models.Model):
7 | artist = models.ForeignKey('artist.artist', models.CASCADE)
8 | name = models.CharField(max_length=100, verbose_name=_('title'))
9 | duration = models.IntegerField(default=320)
10 | owner = models.ForeignKey(
11 | getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
12 | on_delete=models.CASCADE,
13 | )
14 |
15 | class Meta:
16 | ordering = ('name',)
17 |
18 | def __str__(self):
19 | return self.name
20 |
--------------------------------------------------------------------------------
/src/crudlfap_example/song/test_security.py:
--------------------------------------------------------------------------------
1 | """
2 | Test security with crudlfap_example.song setup.
3 |
4 | It's a bit paranoid, but i'll sleep better with this: i don't trust users, i
5 | don't trust myself either :)
6 | """
7 |
8 | import pytest
9 |
10 | from crudlfap import shortcuts as crudlfap
11 | from crudlfap_auth.crudlfap import User
12 | from crudlfap_example.artist.models import Artist
13 | from crudlfap_example.song.models import Song
14 |
15 |
16 | def user(**attrs):
17 | user, created = User.objects.get_or_create(**attrs)
18 | if created:
19 | user.set_password('password')
20 | user.save()
21 | return user
22 |
23 |
24 | def user_client(client, **userattrs):
25 | if not userattrs.get('username', None):
26 | return client
27 | user(**userattrs)
28 | if not client.login(username=userattrs['username'], password='password'):
29 | raise Exception('Could not auth as ' + userattrs['username'])
30 | return client
31 |
32 |
33 | @pytest.fixture
34 | def song0():
35 | user0 = user(username='user0')
36 | artist = Artist.objects.get_or_create(name='artist0')[0]
37 | return Song.objects.get_or_create(
38 | artist=artist, name='song0', owner=user0)[0]
39 |
40 |
41 | cases = [
42 | (dict(), False),
43 | (dict(username='user0'), True),
44 | (dict(username='user1'), False),
45 | (dict(username='staff', is_staff=True), True),
46 | (dict(username='superuser', is_superuser=True), True),
47 | ]
48 |
49 |
50 | @pytest.mark.parametrize('userattrs,expected', cases)
51 | @pytest.mark.parametrize('url', ['detail', 'update', 'delete'])
52 | @pytest.mark.django_db
53 | def test_object_views_object_for_user(client, userattrs, expected, url, song0):
54 | view = crudlfap.site[Song][url].clone(object=song0)
55 | url = view.url
56 | res = user_client(client, **userattrs).get(url)
57 | assert res.status_code == 200 if expected else 404
58 |
59 |
60 | @pytest.mark.parametrize('userattrs,expected', cases)
61 | @pytest.mark.django_db
62 | def test_list_view_get_objects_for_user(client, userattrs, expected, song0):
63 | res = user_client(client, **userattrs).get(crudlfap.site[Song]['list'].url)
64 |
65 | assert res.status_code == 200 if userattrs else 302
66 |
67 | if expected:
68 | assert b'song0' in res.content
69 | else:
70 | assert b'song0' not in res.content
71 |
--------------------------------------------------------------------------------
/src/crudlfap_example/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.urls import include, path, re_path
3 |
4 | from crudlfap import shortcuts as crudlfap
5 |
6 | urlpatterns = [
7 | crudlfap.site.urlpattern,
8 | path('auth/', include('django.contrib.auth.urls')),
9 | path('bundles/', include('ryzom_django.bundle')),
10 | ]
11 |
12 | # CRUDLFA+ extras
13 | if 'crudlfap_registration' in settings.INSTALLED_APPS:
14 | urlpatterns.append(
15 | path(
16 | 'registration/',
17 | include('django_registration.backends.activation.urls')
18 | ),
19 | )
20 |
21 | if 'debug_toolbar' in settings.INSTALLED_APPS and settings.DEBUG:
22 | import debug_toolbar
23 | urlpatterns += [
24 | re_path(r'^__debug__/', include(debug_toolbar.urls)),
25 | ]
26 |
27 | # PUBLIC DEMO mode
28 | from django.contrib.auth.views import LoginView # noqa
29 | from django.contrib.auth.models import User # noqa
30 |
31 |
32 | def login_view(request, *args, **kwargs):
33 | if request.method == 'POST':
34 | for name in ('admin', 'staff', 'user'):
35 | user = User.objects.filter(username=name).first()
36 | if not user:
37 | user = User(username=name, email='user@example.com')
38 | if name == 'staff':
39 | user.is_staff = True
40 | if name == 'admin':
41 | user.is_superuser = True
42 | user.set_password(name)
43 | user.save()
44 | return LoginView.as_view()(request, *args, **kwargs)
45 | urlpatterns.insert(0, path('auth/login/', login_view, name='login')) # noqa
46 |
47 |
48 | from crudlfap_auth.html import * # noqa
49 | @template('registration/login.html', App) # noqa
50 | class DemoLogin(LoginFormViewComponent):
51 | def to_html(self, *content, **context):
52 | return super().to_html(
53 | H3('Demo mode enabled'),
54 | P('Login with either username/password of:'),
55 | Ul(
56 | Li('user/user: for user'),
57 | Li('staff/staff: for staff'),
58 | Li('admin/admin: for superuser (technical stuff should appear)'), # noqa
59 | ),
60 | *content,
61 | **context,
62 | )
63 |
--------------------------------------------------------------------------------
/src/crudlfap_example/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for crudlfap_example project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "crudlfap_example.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/src/crudlfap_registration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_registration/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_registration/crudlfap.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse
2 |
3 | from crudlfap import html
4 |
5 |
6 | def registration(request, menu):
7 | if not request.user.is_authenticated:
8 | menu.insert(1, html.A(
9 | html.MDCListItem('Signup', icon='badge'),
10 | href=reverse('django_registration_register'),
11 | style='text-decoration: none',
12 | ))
13 | return menu
14 |
15 |
16 | html.mdcDrawer.menu_hooks.append(registration)
17 |
--------------------------------------------------------------------------------
/src/crudlfap_registration/html.py:
--------------------------------------------------------------------------------
1 | from django.apps import apps
2 | from django.urls import reverse
3 | from crudlfap.html import * # noqa
4 |
5 | User = apps.get_model(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'))
6 |
7 |
8 | @template('django_registration/registration_form.html', App)
9 | class RegistrationFormViewComponent(FormContainer):
10 | def to_html(self, *content, view, form, **context):
11 | return super().to_html(
12 | *content,
13 | H1('Looks like you need to register', cls='center-text'),
14 | Form(
15 | CSRFInput(view.request),
16 | form,
17 | MDCButtonRaised('Register'),
18 | method='POST',
19 | cls='form card',
20 | ),
21 | )
22 |
23 |
24 | @template('django_registration/registration_complete.html', App)
25 | class RegistrationCompleteNarrowCard(FormContainer):
26 | def __init__(self, **context):
27 | super().__init__(
28 | H4('Check your emails to finish !'),
29 | Div(
30 | 'An activation link has been sent to your email address, '
31 | + 'please open it to finish the signup process.',
32 | style='margin-bottom: 24px',
33 | ),
34 | Div(
35 | 'Then, come back and login to participate to your election.',
36 | style='margin-bottom: 24px;',
37 | ),
38 | cls='card',
39 | style='text-align: center',
40 | )
41 |
42 |
43 | @template('django_registration/activation_complete.html', App)
44 | class ActivationCompleteNarrowCard(FormContainer):
45 | def __init__(self, **context):
46 | super().__init__(
47 | H4('Your account has been activated !'),
48 | Div(
49 | 'You may now ',
50 | A('login', href=reverse('login')),
51 | style='margin-bottom: 24px',
52 | ),
53 | cls='card',
54 | style='text-align: center',
55 | )
56 |
57 |
58 | @template('django_registration/activation_failed.html', App)
59 | class ActivationFailureNarrowCard(FormContainer):
60 | def __init__(self, **context):
61 | super().__init__(
62 | H4('Account activation failure'),
63 | Div(
64 | 'Most likely your account has already been activated.',
65 | style='margin-bottom: 24px',
66 | ),
67 | cls='card',
68 | style='text-align: center',
69 | )
70 |
--------------------------------------------------------------------------------
/src/crudlfap_registration/templates/django_registration/activation_email_body.txt:
--------------------------------------------------------------------------------
1 | Welcome on {{ site.name }} !
2 |
3 | Click below to verify your email, you will then be able to login.
4 |
5 | {{ site.url }}{% url 'django_registration_activate' activation_key %}
6 |
7 | This email as been sended automatically.
8 | Please don't respond at this mail.
9 | Thank's.
10 | Team {{ site.name }}.
11 |
--------------------------------------------------------------------------------
/src/crudlfap_registration/templates/django_registration/activation_email_subject.txt:
--------------------------------------------------------------------------------
1 | Activate your account on Electeez
2 |
--------------------------------------------------------------------------------
/src/crudlfap_registration/test_registration.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import re
3 |
4 |
5 | @pytest.mark.django_db
6 | def test_registration(client, mailoutbox):
7 | assert len(mailoutbox) == 0
8 | response = client.post('/registration/register/', dict(
9 | email='testregistration@example.com',
10 | username='testregistration',
11 | password1='!@aoe@#$',
12 | password2='!@aoe@#$',
13 | ))
14 | assert response.status_code == 302
15 | assert len(mailoutbox) == 1
16 | match = re.search('http://testserver[^\n]*', mailoutbox[0].body)
17 | assert match
18 | activate = match.group()
19 | response = client.get(activate)
20 | assert response.status_code == 302
21 | assert response['Location'] == '/registration/activate/complete/'
22 | response = client.post('/auth/login/', dict(
23 | username='testregistration',
24 | password='!@aoe@#$',
25 | ))
26 | assert response.status_code == 302
27 |
--------------------------------------------------------------------------------
/src/crudlfap_sites/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_sites/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_sites/crudlfap.py:
--------------------------------------------------------------------------------
1 | from crudlfap import shortcuts as crudlfap
2 |
3 | from .models import Site
4 |
5 |
6 | crudlfap.Router(model=Site, icon='language').register()
7 |
--------------------------------------------------------------------------------
/src/crudlfap_sites/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-04-06 09:29
2 |
3 | import django.contrib.sites.models
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ('sites', '0002_alter_domain_unique'),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Site',
19 | fields=[
20 | ('site_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sites.site')),
21 | ('settings', models.JSONField(blank=True, default={})),
22 | ('port', models.PositiveIntegerField(null=True)),
23 | ('protocol', models.CharField(default='http', max_length=5)),
24 | ],
25 | bases=('sites.site',),
26 | managers=[
27 | ('objects', django.contrib.sites.models.SiteManager()),
28 | ],
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/src/crudlfap_sites/migrations/0002_alter_site_settings.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2 on 2021-04-10 15:30
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('crudlfap_sites', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='site',
15 | name='settings',
16 | field=models.JSONField(blank=True, default=dict),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/crudlfap_sites/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourlabs/crudlfap/ae41ef6088be8a6937a950f57d6171df82165cb2/src/crudlfap_sites/migrations/__init__.py
--------------------------------------------------------------------------------
/src/crudlfap_sites/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.sites.models import Site as DjangoSite, SiteManager
2 | from django.core.exceptions import ImproperlyConfigured
3 | from django.db import models
4 | from django.http.request import split_domain_port
5 |
6 |
7 | class Site(DjangoSite):
8 | settings = models.JSONField(blank=True, default=dict)
9 | port = models.PositiveIntegerField(null=True)
10 | protocol = models.CharField(default='http', max_length=5)
11 |
12 | @property
13 | def url(self):
14 | return f'{self.protocol}://{self.domain}'
15 |
16 |
17 | old_get_current = SiteManager.get_current
18 | DjangoSite.objects.model = Site
19 |
20 |
21 | def get_current(self, request=None):
22 | try:
23 | return old_get_current(self, request)
24 | except (ImproperlyConfigured, Site.DoesNotExist):
25 | if not request:
26 | return Site(domain='localhost', name='localhost')
27 | host = request.get_host()
28 | domain, port = split_domain_port(host)
29 | protocol = request.META['wsgi.url_scheme']
30 | Site.objects.create(
31 | name=domain.capitalize(),
32 | domain=host,
33 | port=port or 443 if protocol == 'https' else 80,
34 | protocol=protocol,
35 | )
36 | return old_get_current(self, request)
37 |
38 |
39 | SiteManager.get_current = get_current
40 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{38,39,310}-dj{20,21,30,31,40}
3 |
4 | [testenv]
5 | usedevelop = true
6 |
7 | commands =
8 | pip install -e {toxinidir}[project]
9 | pytest -vv --cov src --cov-report=xml:coverage.xml --cov-report=term-missing --strict -r fEsxXw {posargs:src}
10 |
11 | deps =
12 | pytest
13 | pytest-django
14 | pytest-cov
15 | pytest-mock
16 | dj20: Django>=2.0,<2.1
17 | dj21: Django>=2.1,<2.2
18 | dj30: Django>=3.0,<3.1
19 | dj31: Django>=3.1,<3.2
20 | dj40: Django>=4.0,<4.1
21 |
22 | setenv =
23 | DEBUG=1
24 | PIP_ALLOW_EXTERNAL=true
25 |
26 | [testenv:qa]
27 | commands =
28 | flake8 --show-source --max-complexity=8 --exclude migrations src/ --builtins=ModuleNotFoundError --ignore F405,W503
29 | deps =
30 | flake8
31 | mccabe
32 |
33 | [flake8]
34 | exclude = crudlfap_example
35 | putty-auto-ignore = true
36 | putty-ignore =
37 | crudlfap/shortcuts.py : F401
38 | crudlfap/test_routers.py : D
39 |
40 | [pytest]
41 | testpaths = src
42 | DJANGO_SETTINGS_MODULE = crudlfap_example.settings
43 |
--------------------------------------------------------------------------------