├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .github ├── dependabot.yml └── workflows │ ├── python-package.yml │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yaml ├── .run └── Test All.run.xml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docs ├── index.md └── requirements.txt ├── manage.py ├── mkdocs.yml ├── pyproject.toml ├── termsandconditions ├── __init__.py ├── admin.py ├── apps.py ├── decorators.py ├── forms.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ └── django.po │ └── ru │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── remove_old_version_acceptance.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_termsandconditions_info.py │ ├── 0003_auto_20170627_1217.py │ ├── 0004_auto_20201107_0711.py │ └── __init__.py ├── models.py ├── pipeline.py ├── signals.py ├── static │ └── termsandconditions │ │ ├── css │ │ ├── modal.css │ │ └── view_accept.css │ │ └── js │ │ └── modal.js ├── templates │ └── termsandconditions │ │ ├── snippets │ │ └── termsandconditions.html │ │ ├── tc_accept_terms.html │ │ ├── tc_email_terms.html │ │ ├── tc_email_terms_form.html │ │ ├── tc_print_terms.html │ │ └── tc_view_terms.html ├── templatetags │ ├── __init__.py │ └── terms_tags.py ├── tests.py ├── urls.py └── views.py ├── termsandconditions_demo ├── __init__.py ├── mediaroot │ └── README.txt ├── settings.py ├── settings_local_template.py ├── static │ ├── css │ │ └── termsandconditions.css │ └── images │ │ └── favicon.ico ├── staticroot │ └── README.txt ├── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── error.html │ ├── footer.html │ ├── header.html │ ├── index.html │ ├── index_anon.html │ ├── registration │ │ ├── logged_out.html │ │ └── login.html │ ├── robots.txt │ ├── secure.html │ ├── securetoo.html │ └── terms_required.html ├── urls.py ├── views.py └── wsgi.py └── uv.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/python:0-3.9 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | # [Optional] If your requirements rarely change, uncomment this section to add them to the image. 6 | # COPY requirements.txt /tmp/pip-tmp/ 7 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 8 | # && rm -rf /tmp/pip-tmp 9 | 10 | # [Optional] Uncomment this section to install additional OS packages. 11 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 12 | # && apt-get -y install --no-install-recommends 13 | 14 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Django Terms and Conditions DevContainer", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "app", 5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}" 6 | } 7 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | 9 | volumes: 10 | - ../..:/workspaces:cached 11 | 12 | # Overrides default command so things don't shut down after the process ends. 13 | command: sleep infinity 14 | 15 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 16 | network_mode: service:db 17 | 18 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 19 | # (Adding the "ports" property to this file will not forward from a Codespace.) 20 | 21 | db: 22 | image: postgres:latest 23 | restart: unless-stopped 24 | volumes: 25 | - postgres-data:/var/lib/postgresql/data 26 | environment: 27 | POSTGRES_USER: postgres 28 | POSTGRES_DB: postgres 29 | POSTGRES_PASSWORD: postgres 30 | 31 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. 32 | # (Adding the "ports" property to this file will not forward from a Codespace.) 33 | 34 | volumes: 35 | postgres-data: 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '12:00' 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install uv 23 | uses: astral-sh/setup-uv@v5 24 | with: 25 | cache-dependency-glob: "uv.lock" 26 | enable-cache: true 27 | python-version: ${{ matrix.python-version }} 28 | - run: uv sync 29 | - name: Test 30 | run: | 31 | uv run ruff check . 32 | uv run coverage run manage.py test 33 | uv run coverage xml 34 | - name: codecov-umbrella 35 | uses: codecov/codecov-action@v4 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | file: ./coverage.xml 39 | fail_ci_if_error: false 40 | verbose: true 41 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses UV to upload a new package version when a release is created. 2 | 3 | name: Upload Python Package 4 | 5 | on: 6 | release: 7 | types: [created] 8 | 9 | jobs: 10 | deploy: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.x' 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade uv 23 | uv install setuptools wheel 24 | - name: Build and publish 25 | env: 26 | UV_PUBLISH_USERNAME: ${{ secrets.PYPI_USERNAME }} 27 | UV_PUBLISH_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 28 | run: | 29 | uv build 30 | uv publish 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # lines that begin with # are comments. 2 | *.db 3 | *.db-journal 4 | *.egg-info 5 | *.log 6 | *.py[co] 7 | *-env 8 | *.env 9 | *_build 10 | .coverage 11 | .DS_Store 12 | .idea 13 | .project 14 | .pydevproject 15 | .pydevproject.bak 16 | .settings 17 | .tmp_* 18 | .tox 19 | build/* 20 | local_settings.py 21 | nosetests.xml 22 | reports 23 | settings_local.py 24 | settings_local_test.py 25 | settings_test_local.py 26 | style-*_.css 27 | #StaticFiles Consolidated Location 28 | static_root/* 29 | !static_root/README.txt 30 | *.virtualenv 31 | *.virtualenv3 32 | *.virtualenv36 33 | *.venv* 34 | venv* 35 | /site/ 36 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | fail_on_warning: false 17 | 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | -------------------------------------------------------------------------------- /.run/Test All.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Tim L. White and other contributors 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 | 7 | 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. 8 | 9 | 3. 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 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | recursive-include termsandconditions/templates * 3 | recursive-include termsandconditions/templatetags * 4 | recursive-include termsandconditions/static * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Terms and Conditions 2 | =========================== 3 | 4 | [![PyPi Package Version](https://badge.fury.io/py/django-termsandconditions.svg)](http://badge.fury.io/py/django-termsandconditions) [![Actions Status](https://github.com/cyface/django-termsandconditions/workflows/Python%20package/badge.svg)](https://github.com/cyface/django-termsandconditions/actions) [![codecov](https://codecov.io/gh/cyface/django-termsandconditions/branch/master/graph/badge.svg?token=RvtjZ2bngZ)](https://codecov.io/gh/cyface/django-termsandconditions) [![docs](https://readthedocs.org/projects/django-termsandconditions/badge/)](https://django-termsandconditions.readthedocs.io/) 5 | 6 | Django Terms and Conditions gives you an configurable way to send users 7 | to a T&C acceptance page before they can access the site. 8 | 9 | *Note that version 2.0+ requires Python 3.7+ and Django 2.2+.* 10 | 11 | *Newer releases have higher version requirements* 12 | 13 | Creator and Maintainer: - Tim White () 14 | 15 | Contributors: - Adibo () - Nathan Swain () 16 | 17 | Features 18 | -------- 19 | 20 | This module is meant to be as quick to integrate as possible, and thus 21 | extensive customization will likely benefit from a fork. That said, a 22 | number of options are available. Currently, the app allows for 23 | 24 | - terms-and-conditions versioning (via version\_number) 25 | - multiple terms-and-conditions allowed (via slug field) 26 | - per-user terms-and-conditions acceptance 27 | - middleware to take care of redirecting to proper 28 | terms-and-conditions acceptance page upon the version change 29 | - multi-language support 30 | 31 | Installation 32 | ------------ 33 | 34 | **Note that version 2.0+ of django-termsandconditions only works with Python 3.6+ and Django 2.2+** 35 | 36 | From [pypi](https://pypi.python.org): 37 | 38 | $ pip install django-termsandconditions 39 | 40 | or: 41 | 42 | $ easy_install django-termsandconditions 43 | 44 | or clone from [github](http://github.com): 45 | 46 | $ git clone git://github.com/cyface/django-termsandconditions.git 47 | 48 | and add django-termsandconditions to the `PYTHONPATH`: 49 | 50 | $ export PYTHONPATH=$PYTHONPATH:$(pwd)/django-termsandconditions/ 51 | 52 | or: 53 | 54 | $ cd django-termsandconditions 55 | $ sudo python setup.py install 56 | 57 | Demo App 58 | -------- 59 | 60 | The termsandconditions\_demo app is included to quickly let you see how 61 | to get a working installation going. 62 | 63 | The demo is built as a mobile app using 64 | [jQueryMobile](http://jquerymobile.com/) loaded from the jQuery CDN. 65 | 66 | Take a look at the `requirements.txt` file in the 67 | `termsandconditions_demo` directory for a quick way to use pip to 68 | install all the needed dependencies: 69 | 70 | $ pip install -r requirements.txt 71 | 72 | The `settings_main.py`, file has a working configuration you can crib 73 | from. 74 | 75 | The templates in the `termsandconditions/templates`, and 76 | `termsandconditions_demo/templates` directories give you a good idea of 77 | the kinds of things you will need to do if you want to provide a custom 78 | interface. 79 | 80 | Configuration 81 | ------------- 82 | 83 | Configuration is minimal for termsandconditions itself, A quick guide to 84 | a basic setup is below, take a look at the demo app's settings.py for 85 | more details. 86 | 87 | Some useful settings: 88 | : - TERMS\_IP\_HEADER\_NAME Name of header to check for IP address. 89 | Defaults to 'REMOTE\_ADDR'. You might need to use 90 | 'HTTP\_X\_FORWARDED\_FOR', or other headers in proxy setups. 91 | - TERMS\_STORE\_IP\_ADDRESS - True/False whether to store IPs with 92 | Terms Acceptance 93 | 94 | ### Requirements 95 | 96 | The app needs `django>=2.2`. 97 | 98 | ### Add INSTALLED\_APPS 99 | 100 | Add termsandconditions to installed applications: 101 | 102 | INSTALLED_APPS = ( 103 | ... 104 | 'termsandconditions', 105 | ) 106 | 107 | ### Add urls to urls.py 108 | 109 | In your urls.py, you need to pull in the termsandconditions and/or 110 | termsandconditions urls: 111 | 112 | # Terms and Conditions 113 | url(r'^terms/', include('termsandconditions.urls')), 114 | 115 | Terms and Conditions 116 | -------------------- 117 | 118 | You will need to set up a Terms and Conditions entry in the admin (or 119 | via direct DB load) for users to accept if you want to use the T&C 120 | module. 121 | 122 | ### Terms and Conditions Versioning 123 | 124 | Note that the versions and dates of T&Cs are important. You can create a 125 | new version of a T&C with a future date, and once that date is in the 126 | past, it will force users to accept that new version of the T&Cs. 127 | 128 | ### Terms and Conditions Default URLs 129 | 130 | If you have included the terms urls under **/terms**, these URLs would 131 | all be prefixed by that (e.g. /terms/accept/). 132 | 133 | - **/** - List all terms that have not been accepted 134 | - **/accept/** - List all terms that have not been accepted with 135 | accept links 136 | - **/accept/\/** - Show page to accept latest version of a 137 | specific terms 138 | - **/accept/\/\/** - Show page to accept a specific 139 | version of a specific terms 140 | - **/active/** - List all active terms 141 | - **/email/** - Show page to email all unaccepted terms 142 | - **/email/\/\/** - Show page to email specific 143 | version of specific terms 144 | - **/view/\/** - View the latest version of a specific terms 145 | - **/view/\/\/** - View a specific version of a 146 | specific terms 147 | 148 | ### Terms and Conditions Middleware 149 | 150 | You can force protection of your whole site by using the T&C middleware. 151 | Once activated, any attempt to access an authenticated page will first 152 | check to see if the user has accepted the active T&Cs. This can be a 153 | performance impact, so you can also use the 154 | \_TermsAndConditionsDecorator to protect specific views, or the pipeline 155 | setup to only check on account creation. 156 | 157 | Here is the middleware configuration: 158 | 159 | MIDDLEWARE_CLASSES = ( 160 | ... 161 | 'termsandconditions.middleware.TermsAndConditionsRedirectMiddleware', 162 | 163 | By default, some pages are excluded from the middleware, you can 164 | configure exclusions with these settings: 165 | 166 | ACCEPT_TERMS_PATH = '/terms/accept/' 167 | TERMS_EXCLUDE_URL_PREFIX_LIST = {'/admin/',}) 168 | TERMS_EXCLUDE_URL_LIST = {'/', '/terms/required/', '/logout/', '/securetoo/'} 169 | TERMS_EXCLUDE_URL_CONTAINS_LIST = {} 170 | 171 | TERMS\_EXCLUDE\_URL\_PREFIX\_LIST is a list of 'starts with' strings to 172 | exclude, while TERMS\_EXCLUDE\_URL\_LIST is a list of explicit full 173 | paths to exclude. TERMS\_EXCLUDE\_URL\_CONTAINS\_LIST is a list of url 174 | fragments to check, if the url 'contains' that string, it is excluded. 175 | This can be particularly useful for i18n, where your url could get 176 | prepended with a language code. 177 | 178 | You can also define a setting TERMS\_EXCLUDE\_USERS\_WITH\_PERM to 179 | exclude users with a custom permission you create yourself.: 180 | 181 | TERMS_EXCLUDE_USERS_WITH_PERM = 'MyModel.can_skip_terms' 182 | 183 | This can be useful if you need to run continuous login integration tests 184 | or simply exclude specific users from having to accept your T&Cs. Note 185 | that we exclude superusers by default from this check due to Django's 186 | has\_perm() method returning True for any permission check, so adding 187 | this permission to a superuser has no effect. If you want to exclude 188 | superusers you can set TERMS\_EXCLUDE\_SUPERUSERS: 189 | 190 | TERMS_EXCLUDE_SUPERUSERS = True 191 | 192 | ### Terms and Conditions Useful Methods 193 | 194 | - **TermsAndConditions.get\_active\_terms\_list()** - Returns a list 195 | of all active terms (accepted by current user or not) 196 | - **TermsAndConditions.get\_active\_terms\_not\_agreed\_to(\)** 197 | - Returns a list of terms the specified user has not agreed to 198 | - **TermsAndConditions.get\_active(\)** - Returns the active 199 | terms of the specified terms slug 200 | 201 | ### Terms and Conditions Cache 202 | 203 | To speed performance, especially for the middleware, the terms and their 204 | acceptance are cached. 205 | 206 | You can control how long they are cached (or if they are cached at all) 207 | with this setting: 208 | 209 | TERMS_CACHE_SECONDS = 30 210 | 211 | A numeric value is the number of seconds that the terms and their 212 | acceptance should be cached (default 30). If set to 0, values will never 213 | be cached. 214 | 215 | ### Terms and Conditions View Decorator 216 | 217 | You can protect only specific views with T&Cs using the 218 | @terms\_required() decorator at the top of a function like this: 219 | 220 | from termsandconditions.decorators import terms_required 221 | 222 | @login_required 223 | @terms_required 224 | def terms_required_view(request): 225 | ... 226 | 227 | Note that you can skip @login\_required only if you are forcing auth on 228 | that view in some other way. 229 | 230 | Requiring T&Cs for Anonymous Users is not supported. 231 | 232 | Many of the templates extend the 'base.html' template by default. The 233 | TERMS\_BASE\_TEMPLATE setting can be used to specify a different 234 | template to extend: 235 | 236 | TERMS_BASE_TEMPLATE = 'page.html' 237 | 238 | A bare minimum template that can be used is the following: 239 | 240 | 241 | 242 | 243 | [My Title] 244 | {% block styles %}{% endblock %} 245 | 246 | 247 | 248 |
249 |

{% block title %}{% endblock %}

250 | {% block content %}{% endblock %} 251 |
252 | 253 | 254 | 255 | ### Terms and Conditions Template Tag 256 | 257 | To facilitate support of terms changes without a direct redirection to 258 | the `/terms/accept` url, a template tag is supplied for convenience. 259 | Thus, instead of using e.g. the `TermsAndConditionsRedirectMiddleware` 260 | one can use the template tag. The template tag will take care that a 261 | proper modal is shown to the user informing a user that new terms have 262 | been set and need to be accepted. To use the template tag, do the 263 | following. In your template (for example in base.html), include the 264 | following lines: 265 | 266 | {% load terms_tags %} 267 | .... your template here .... 268 | 269 | {% show_terms_if_not_agreed %} 270 | 271 | Alternatively use: 272 | 273 | {% load terms_tags %} 274 | .... your template here .... 275 | 276 | {% show_terms_if_not_agreed field='HTTP_REFERER' %} 277 | 278 | if you want other than default `TERMS_HTTP_PATH_FIELD` to be used (this 279 | can also be controlled via settings, see below). This will ensure that 280 | on every page using the template (that is on each page using base.html 281 | in this case), respective T&C css and js are loaded to take care for 282 | handling the modal. 283 | 284 | The modal will show the basic information about the new terms as well as 285 | a link to page which enables the user to accept these terms. Please note 286 | that a user may wish not to accept terms and close the modal. In such a 287 | case, the modal will be shown again as soon as another view with the 288 | template including the template tag is called. This simple mechanism 289 | allows to nag users with new T&C while still allowing them to use the 290 | service, without instant redirections. 291 | 292 | The following configuration setting applies for the template tag: 293 | 294 | TERMS_HTTP_PATH_FIELD = 'PATH_INFO' 295 | 296 | which defaults to `PATH_INFO`. When needed (e.g. while using a separate 297 | AJAX view to take care for the modal) this can be changed to 298 | `HTTP_REFERER`. 299 | 300 | ### Using terms with as\_template filter 301 | 302 | If you happen to use termsandconditions which text field includes some 303 | template tags (e.g. `{% url 'you-url' %}`), you may want to render its 304 | content, before including it into your template. To achieve this goal, 305 | use `include` with the `as_template` filter, i.e.: 306 | 307 | {% load terms_tags %} 308 | .... your template here .... 309 | 310 | {% include terms|as_template %} 311 | 312 | Note, that you need to modify the default termsandconditions templates, 313 | as the default ones use terms as template variable. 314 | 315 | ### Terms and Conditions Pipeline 316 | 317 | You can force T&C acceptance when a new user account is created using 318 | the django-socialauth pipeline: 319 | 320 | SOCIAL_AUTH_PIPELINE = ( 321 | 'social_auth.backends.pipeline.social.social_auth_user', 322 | 'social_auth.backends.pipeline.associate.associate_by_email', 323 | 'social_auth.backends.pipeline.user.get_username', 324 | 'social_auth.backends.pipeline.user.create_user', 325 | 'social_auth.backends.pipeline.social.associate_user', 326 | 'social_auth.backends.pipeline.social.load_extra_data', 327 | 'social_auth.backends.pipeline.misc.save_status_to_session', 328 | 'termsandconditions.pipeline.user_accept_terms', 329 | ) 330 | 331 | Note that the configuration above also prevents django-socialauth from 332 | updating profile data from the social backends once a profile is 333 | created, due to: 334 | 335 | 'social_auth.backends.pipeline.user.update_user_details' 336 | 337 | ...not being included in the pipeline. This is wise behavior when you 338 | are letting users update their own profile details. 339 | 340 | This pipeline configuration will send users to the '/terms/accept' page 341 | right before sending them on to whatever you have set 342 | SOCIAL\_AUTH\_NEW\_USER\_REDIRECT\_URL to. However, it will not, without 343 | the middleware or decorators described above, check that the user has 344 | accepted the latest T&Cs before letting them continue on to viewing the 345 | site. 346 | 347 | You can use the various T&C methods in concert depending on your needs. 348 | 349 | Multi-Language Support 350 | ---------------------- 351 | 352 | In case you are in need of your `termsandconditions` objects to handle 353 | multiple languages, we recommend to use 354 | `django-modeltranslation ` 355 | (or similar) module. In case of django-modeltranslation the setup is 356 | rather straight forward, but needs several steps. Here they are. 357 | 358 | ### 1. Modify your `settings.py` 359 | 360 | In your `settings.py` file, you need to specify the `LANGUAGES` and set 361 | `MIGRATION_MODULES` to point to a local migration directory for the 362 | `termsandconditions` module (the migration due to modeltranslation will 363 | live there): 364 | 365 | LANGUAGES = ( 366 | ('en', 'English'), 367 | ('pl', 'Polish'), 368 | ) 369 | 370 | MIGRATION_MODULES = { 371 | # local path for migration for the termsandconditions 372 | 'termsandconditions': 'your_app.migrations.migrations_termsandconditions', 373 | } 374 | 375 | Don't forget to create the respective directory and the `__init__.py` 376 | file there! Please note that `migrations_termsandconditions` directory 377 | name is used to avoid confusion with the T&C app name. 378 | 379 | You will also need to add `modeltranslation` to `INSTALLED_APPS` in your 380 | `settings.py`. You also need to ensure the module that you added your translations.py file to is in ``INSTALLED_APPS``. 381 | 382 | ### 2. Make initial local migration 383 | 384 | As we switch to the local migration for the `termsandconditions` module, 385 | we need to execute initial migration for the module (as a starting 386 | point). Thus: 387 | 388 | python manage.py makemigrations termsandconditions 389 | 390 | The relevant initial migration file should now be in 391 | `your_app/migrations/migrations_termsandconditions` directory. Now, just 392 | execute the migration: 393 | 394 | python manage.py migrate termsandconditions 395 | 396 | ### 3. Add translation 397 | 398 | To translate terms-and-conditions model to other languages (as specified 399 | in `settings.py`), create a `translation.py` file in your project, with 400 | the following content: 401 | 402 | from modeltranslation.translator import translator, TranslationOptions 403 | from termsandconditions.models import TermsAndConditions 404 | 405 | class TermsAndConditionsTranslationOptions(TranslationOptions): 406 | fields = ('name', 'text', 'info') 407 | translator.register(TermsAndConditions, TermsAndConditionsTranslationOptions) 408 | 409 | This assumes you want to have 3 most relevant model fields translated. 410 | After that you just need to make migrations again (to account for new 411 | fields due to modeltranslation): 412 | 413 | python manage.py makemigrations termsandconditions 414 | python manage.py migrate termsandconditions 415 | 416 | Your model is now ready to cover the translations! Just as 417 | hint we suggest to also include some data migration in order to populate 418 | newly created, translated fields (i.e. `name_en`, `name_pl`, etc.) with 419 | the initial data (e.g. by copying the content of the base field, i.e. 420 | `name`, etc.) 421 | 422 | ### 4. Add ``/terms/`` to the ``TERMS_EXCLUDE_URL_CONTAINS_LIST`` setting. 423 | In order to prevent redirect loops, if you are using internationalized URLs, you will need to add add: 424 | 425 | ``TERMS_EXCLUDE_URL_CONTAINS_LIST = {'/terms/', '/i18n/setlang/', }`` 426 | 427 | to your ``settings.py`` to prevent redirect loops with the language-code-prepended URLs (e.g. ``/en/terms/``) 428 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.6.1 -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "termsandconditions_demo.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Django Terms and Conditions 2 | 3 | theme: 4 | name: readthedocs 5 | highlightjs: true 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-termsandconditions" 3 | version = "2.0.12" 4 | description = "Django app that enables users to accept terms and conditions of a site." 5 | authors = [{ name = "Tim White", email = "tim@cyface.com" }] 6 | requires-python = ">=3.8,<4.0" 7 | readme = "README.md" 8 | license = { text = "BSD License" } 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Framework :: Django", 12 | "Intended Audience :: Developers", 13 | "License :: OSI Approved :: MIT License", 14 | "Operating System :: OS Independent", 15 | "Programming Language :: Python :: 3.6", 16 | "Programming Language :: Python :: 3.7", 17 | "Programming Language :: Python :: 3.8", 18 | "Topic :: Internet :: WWW/HTTP", 19 | ] 20 | dependencies = ["Django>2.2"] 21 | 22 | [project.urls] 23 | Homepage = "https://github.com/cyface/django-termsandconditions" 24 | 25 | [tool.black] 26 | line-length = 88 27 | target-version = ['py38', 'py39', 'py310', 'py311'] 28 | include = '\.pyi?$' 29 | exclude = ''' 30 | /( 31 | \.eggs 32 | | \.git 33 | | \.hg 34 | | \.mypy_cache 35 | | \.tox 36 | | \.venv 37 | | _build 38 | | buck-out 39 | | build 40 | | dist 41 | | migrations 42 | )/ 43 | ''' 44 | 45 | [dependency-groups] 46 | dev = [ 47 | "black~=24.8", 48 | "coverage[toml]~=7.6", 49 | "ruff>=0.9.0,<0.10", 50 | "uv>=0.5" 51 | ] 52 | 53 | [tool.coverage.run] 54 | branch = true 55 | omit = [ 56 | "*/__init__.py", 57 | "*/apps.py", 58 | "*/devscripts*", 59 | "*/docs/*", 60 | "*/local_settings_template.py", 61 | "*/manage.py", 62 | "*/migrations/*", 63 | "*/settings*", 64 | "*/setup.py", 65 | "*/tests*", 66 | "*/wsgi.py" 67 | ] 68 | 69 | [tool.hatch.build.targets.sdist] 70 | include = ["termsandconditions"] 71 | 72 | [tool.hatch.build.targets.wheel] 73 | include = ["termsandconditions"] 74 | 75 | [build-system] 76 | requires = ["hatchling"] 77 | build-backend = "hatchling.build" 78 | 79 | [tool.ruff] 80 | extend-exclude = ["migrations"] 81 | lint.ignore = [ 82 | "B904", 83 | "DJ001", 84 | "DJ008", 85 | "DJ012", 86 | "E501", 87 | "E722", 88 | "F403", 89 | "F405", 90 | "N806", 91 | "N815", 92 | ] 93 | lint.select = [ 94 | "B", 95 | "B9", 96 | "C", 97 | "DJ", 98 | "DTZ", 99 | "E", 100 | "F", 101 | "N", 102 | "UP", 103 | "W", 104 | ] 105 | line-length = 88 106 | 107 | [tool.ruff.lint.isort] 108 | forced-separate = ["django"] 109 | 110 | [tool.ruff.lint.mccabe] 111 | max-complexity = 25 112 | -------------------------------------------------------------------------------- /termsandconditions/__init__.py: -------------------------------------------------------------------------------- 1 | """Django Terms and Conditions Module""" 2 | default_app_config = "termsandconditions.apps.TermsAndConditionsConfig" 3 | -------------------------------------------------------------------------------- /termsandconditions/admin.py: -------------------------------------------------------------------------------- 1 | """Django Admin Site configuration""" 2 | 3 | from django.contrib import admin 4 | from django.utils.translation import gettext as _ 5 | from .models import TermsAndConditions, UserTermsAndConditions 6 | 7 | 8 | class TermsAndConditionsAdmin(admin.ModelAdmin): 9 | """Sets up the custom Terms and Conditions admin display""" 10 | 11 | list_display = ( 12 | "slug", 13 | "name", 14 | "date_active", 15 | "version_number", 16 | ) 17 | verbose_name = _("Terms and Conditions") 18 | 19 | 20 | class UserTermsAndConditionsAdmin(admin.ModelAdmin): 21 | """Sets up the custom User Terms and Conditions admin display""" 22 | 23 | # fields = ('terms', 'user', 'date_accepted', 'ip_address',) 24 | readonly_fields = ("date_accepted",) 25 | list_display = ( 26 | "terms", 27 | "user", 28 | "date_accepted", 29 | "ip_address", 30 | ) 31 | date_hierarchy = "date_accepted" 32 | list_select_related = True 33 | 34 | 35 | admin.site.register(TermsAndConditions, TermsAndConditionsAdmin) 36 | admin.site.register(UserTermsAndConditions, UserTermsAndConditionsAdmin) 37 | -------------------------------------------------------------------------------- /termsandconditions/apps.py: -------------------------------------------------------------------------------- 1 | """Django Apps Config""" 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import gettext_lazy as _ 5 | import logging 6 | 7 | LOGGER = logging.getLogger(name="termsandconditions") 8 | 9 | 10 | class TermsAndConditionsConfig(AppConfig): 11 | """App config for TermsandConditions""" 12 | 13 | name = "termsandconditions" 14 | verbose_name = _("Terms and Conditions") 15 | 16 | def ready(self): 17 | import termsandconditions.signals # noqa F401 18 | -------------------------------------------------------------------------------- /termsandconditions/decorators.py: -------------------------------------------------------------------------------- 1 | """View Decorators for termsandconditions module""" 2 | from urllib.parse import urlparse, urlunparse 3 | 4 | from functools import wraps 5 | from django.http import HttpResponseRedirect, QueryDict 6 | from .models import TermsAndConditions 7 | from .middleware import ACCEPT_TERMS_PATH 8 | 9 | 10 | def terms_required(view_func): 11 | """ 12 | This decorator checks to see if the user is logged in, and if so, if they have accepted the site terms. 13 | """ 14 | 15 | @wraps(view_func) 16 | def _wrapped_view(request, *args, **kwargs): 17 | """Method to wrap the view passed in""" 18 | # If user has not logged in, or if they have logged in and already agreed to the terms, let the view through 19 | if ( 20 | not request.user.is_authenticated 21 | or not TermsAndConditions.get_active_terms_not_agreed_to(request.user) 22 | ): 23 | return view_func(request, *args, **kwargs) 24 | 25 | # Otherwise, redirect to terms accept 26 | current_path = request.path 27 | login_url_parts = list(urlparse(ACCEPT_TERMS_PATH)) 28 | querystring = QueryDict(login_url_parts[4], mutable=True) 29 | querystring["returnTo"] = current_path 30 | login_url_parts[4] = querystring.urlencode(safe="/") 31 | return HttpResponseRedirect(urlunparse(login_url_parts)) 32 | 33 | return _wrapped_view 34 | -------------------------------------------------------------------------------- /termsandconditions/forms.py: -------------------------------------------------------------------------------- 1 | """Django forms for the termsandconditions application""" 2 | 3 | from django import forms 4 | from django.db.models import QuerySet 5 | 6 | from termsandconditions.models import TermsAndConditions 7 | 8 | 9 | class UserTermsAndConditionsModelForm(forms.Form): 10 | """Form used when accepting Terms and Conditions - returnTo is used to catch where to end up.""" 11 | 12 | returnTo = forms.CharField(required=False, initial="/", widget=forms.HiddenInput()) 13 | terms = forms.ModelMultipleChoiceField( 14 | TermsAndConditions.objects.none(), 15 | widget=forms.MultipleHiddenInput, 16 | ) 17 | 18 | def __init__(self, *args, **kwargs): 19 | kwargs.pop("instance", None) 20 | 21 | terms_list = kwargs.get("initial", {}).get("terms", None) 22 | 23 | if terms_list is None: # pragma: nocover 24 | terms_list = TermsAndConditions.get_active_terms_list() 25 | 26 | if terms_list is QuerySet: 27 | self.terms = forms.ModelMultipleChoiceField( 28 | terms_list, widget=forms.MultipleHiddenInput 29 | ) 30 | else: 31 | self.terms = terms_list 32 | 33 | super().__init__(*args, **kwargs) 34 | 35 | 36 | class EmailTermsForm(forms.Form): 37 | """Form used to collect email address to send terms and conditions to.""" 38 | 39 | email_subject = forms.CharField(widget=forms.HiddenInput()) 40 | email_address = forms.EmailField() 41 | returnTo = forms.CharField(required=False, initial="/", widget=forms.HiddenInput()) 42 | terms = forms.ModelChoiceField( 43 | queryset=TermsAndConditions.objects.all(), widget=forms.HiddenInput() 44 | ) 45 | -------------------------------------------------------------------------------- /termsandconditions/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyface/django-termsandconditions/8a2902539f51985ff81574f1f86cff8e464d6b12/termsandconditions/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /termsandconditions/locale/en/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-03-19 21:03+0500\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 | #: admin.py:13 apps.py:15 templates/termsandconditions/tc_accept_terms.html:13 22 | #: templates/termsandconditions/tc_email_terms_form.html:8 23 | #: templates/termsandconditions/tc_view_terms.html:13 24 | msgid "Terms and Conditions" 25 | msgstr "" 26 | 27 | #: models.py:25 28 | msgid "IP Address" 29 | msgstr "" 30 | 31 | #: models.py:26 32 | msgid "Date Accepted" 33 | msgstr "" 34 | 35 | #: models.py:31 models.py:32 36 | msgid "User Terms and Conditions" 37 | msgstr "" 38 | 39 | #: models.py:48 40 | msgid "Provide users with some info about what's changed and why" 41 | msgstr "" 42 | 43 | #: models.py:50 44 | msgid "Leave Null To Never Make Active" 45 | msgstr "" 46 | 47 | #: templates/termsandconditions/snippets/termsandconditions.html:16 48 | msgid "Close" 49 | msgstr "" 50 | 51 | #: templates/termsandconditions/snippets/termsandconditions.html:29 52 | msgid "ACCEPT ALL" 53 | msgstr "" 54 | 55 | #: templates/termsandconditions/tc_accept_terms.html:6 56 | msgid "Accept Terms and Conditions" 57 | msgstr "" 58 | 59 | #: templates/termsandconditions/tc_accept_terms.html:16 60 | msgid "Please Accept" 61 | msgstr "" 62 | 63 | #: templates/termsandconditions/tc_accept_terms.html:18 64 | msgid "Summary of Changes" 65 | msgstr "" 66 | 67 | #: templates/termsandconditions/tc_accept_terms.html:22 68 | msgid "Full Text" 69 | msgstr "" 70 | 71 | #: templates/termsandconditions/tc_accept_terms.html:31 72 | #: templates/termsandconditions/tc_view_terms.html:21 73 | msgid "Print" 74 | msgstr "" 75 | 76 | #: templates/termsandconditions/tc_accept_terms.html:37 77 | msgid "Accept" 78 | msgstr "" 79 | 80 | #: templates/termsandconditions/tc_accept_terms.html:37 81 | msgid "All" 82 | msgstr "" 83 | 84 | #: templates/termsandconditions/tc_email_terms.html:5 85 | #: templates/termsandconditions/tc_print_terms.html:29 86 | msgid "Version" 87 | msgstr "" 88 | 89 | #: templates/termsandconditions/tc_email_terms_form.html:5 90 | msgid "Email Terms and Conditions" 91 | msgstr "" 92 | 93 | #: templates/termsandconditions/tc_email_terms_form.html:9 94 | msgid "Email" 95 | msgstr "" 96 | 97 | #: templates/termsandconditions/tc_email_terms_form.html:12 98 | msgid "Email To:" 99 | msgstr "" 100 | 101 | #: templates/termsandconditions/tc_email_terms_form.html:14 102 | msgid "You Requested:" 103 | msgstr "" 104 | 105 | #: templates/termsandconditions/tc_email_terms_form.html:15 106 | msgid "Send" 107 | msgstr "" 108 | 109 | #: templates/termsandconditions/tc_print_terms.html:6 110 | msgid "Print Terms and Conditions" 111 | msgstr "" 112 | 113 | #: templates/termsandconditions/tc_view_terms.html:6 114 | msgid "View Terms and Conditions" 115 | msgstr "" 116 | 117 | #: views.py:171 118 | msgid "Terms" 119 | msgstr "" 120 | 121 | #: views.py:176 122 | msgid "Terms and Conditions Sent." 123 | msgstr "" 124 | 125 | #: views.py:178 126 | msgid "An Error Occurred Sending Your Message." 127 | msgstr "" 128 | 129 | #: views.py:187 130 | msgid "Invalid Email Address." 131 | msgstr "" 132 | -------------------------------------------------------------------------------- /termsandconditions/locale/pt_BR/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 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Django Terms and Conditions\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2020-09-30 18:21-0300\n" 11 | "PO-Revision-Date: 2020-09-30 16:27-0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | "Language: pt_BR\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: .\termsandconditions\admin.py:19 .\termsandconditions\apps.py:16 21 | #: .\termsandconditions\models.py:97 .\termsandconditions\models.py:98 22 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:13 23 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms_form.html:8 24 | #: .\termsandconditions\templates\termsandconditions\tc_view_terms.html:13 25 | msgid "Terms and Conditions" 26 | msgstr "Termos e Condições" 27 | 28 | #: .\termsandconditions\models.py:29 29 | msgid "user" 30 | msgstr "usuário" 31 | 32 | #: .\termsandconditions\models.py:35 33 | msgid "terms" 34 | msgstr "termos" 35 | 36 | #: .\termsandconditions\models.py:38 37 | msgid "IP Address" 38 | msgstr "Endereço de IP" 39 | 40 | #: .\termsandconditions\models.py:41 41 | msgid "Date Accepted" 42 | msgstr "Data do Aceite" 43 | 44 | #: .\termsandconditions\models.py:48 .\termsandconditions\models.py:49 45 | msgid "User Terms and Conditions" 46 | msgstr "Aceite dos Termos e Condições Pelos Usuários" 47 | 48 | #: .\termsandconditions\models.py:66 49 | msgid "name" 50 | msgstr "nome" 51 | 52 | #: .\termsandconditions\models.py:74 53 | msgid "version number" 54 | msgstr "número da versão" 55 | 56 | #: .\termsandconditions\models.py:76 57 | msgid "text" 58 | msgstr "texto" 59 | 60 | #: .\termsandconditions\models.py:80 61 | msgid "Provide users with some info about what's changed and why" 62 | msgstr "Forneça aos usuários algumas informações sobre o que mudou e por quê" 63 | 64 | #: .\termsandconditions\models.py:81 65 | msgid "info" 66 | msgstr "informações" 67 | 68 | #: .\termsandconditions\models.py:85 69 | msgid "Leave Null To Never Make Active" 70 | msgstr "Deixar Nulo Para Nunca Ativar" 71 | 72 | #: .\termsandconditions\models.py:86 73 | msgid "date active" 74 | msgstr "data de ativação" 75 | 76 | #: .\termsandconditions\templates\termsandconditions\snippets\termsandconditions.html:15 77 | msgid "Our Terms and Conditions Have Changed" 78 | msgstr "Nossos Termos e Condições Mudaram" 79 | 80 | #: .\termsandconditions\templates\termsandconditions\snippets\termsandconditions.html:16 81 | msgid "Close" 82 | msgstr "Fechar" 83 | 84 | #: .\termsandconditions\templates\termsandconditions\snippets\termsandconditions.html:26 85 | msgid "Please, review and accept the changes to prevent future interruptions." 86 | msgstr "Por favor, revise e aceite as alterações para evitar interrupções futuras." 87 | 88 | #: .\termsandconditions\templates\termsandconditions\snippets\termsandconditions.html:29 89 | msgid "ACCEPT ALL" 90 | msgstr "ACEITAR TODOS" 91 | 92 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:6 93 | msgid "Accept Terms and Conditions" 94 | msgstr "Aceitar Termos e Condições" 95 | 96 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:16 97 | msgid "Please Accept" 98 | msgstr "Por Favor Aceite" 99 | 100 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:18 101 | msgid "Summary of Changes" 102 | msgstr "Sumário de Alterações" 103 | 104 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:22 105 | msgid "Full Text" 106 | msgstr "Texto Completo" 107 | 108 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:32 109 | #: .\termsandconditions\templates\termsandconditions\tc_view_terms.html:22 110 | msgid "Print" 111 | msgstr "Imprimir" 112 | 113 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:39 114 | msgid "Accept" 115 | msgstr "Aceitar" 116 | 117 | #: .\termsandconditions\templates\termsandconditions\tc_accept_terms.html:39 118 | msgid "All" 119 | msgstr "Todos" 120 | 121 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms.html:5 122 | #: .\termsandconditions\templates\termsandconditions\tc_print_terms.html:29 123 | msgid "Version" 124 | msgstr "Versão" 125 | 126 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms_form.html:5 127 | msgid "Email Terms and Conditions" 128 | msgstr "Enviar Termos e Condições por Email" 129 | 130 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms_form.html:9 131 | msgid "Email" 132 | msgstr "Email" 133 | 134 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms_form.html:12 135 | msgid "Email To:" 136 | msgstr "Email Para:" 137 | 138 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms_form.html:14 139 | msgid "You Requested:" 140 | msgstr "Você Solicitou:" 141 | 142 | #: .\termsandconditions\templates\termsandconditions\tc_email_terms_form.html:15 143 | msgid "Send" 144 | msgstr "Enviar" 145 | 146 | #: .\termsandconditions\templates\termsandconditions\tc_print_terms.html:6 147 | msgid "Print Terms and Conditions" 148 | msgstr "Imprimir Termos e Condições" 149 | 150 | #: .\termsandconditions\templates\termsandconditions\tc_view_terms.html:6 151 | msgid "View Terms and Conditions" 152 | msgstr "Visualizar Termos e Condições" 153 | 154 | #: .\termsandconditions\templates\termsandconditions\tc_view_terms.html:24 155 | msgid "No terms defined." 156 | msgstr "Nenhum termo definido." 157 | 158 | #: .\termsandconditions\templates\termsandconditions\tc_view_terms.html:27 159 | msgid "No terms for you to accept." 160 | msgstr "Nenhum termo pendente de aceitação." 161 | 162 | #: .\termsandconditions\views.py:160 163 | msgid "Terms" 164 | msgstr "Termos" 165 | 166 | #: .\termsandconditions\views.py:167 167 | msgid "Terms and Conditions Sent." 168 | msgstr "Termos e Condições Enviados." 169 | 170 | #: .\termsandconditions\views.py:173 171 | msgid "An Error Occurred Sending Your Message." 172 | msgstr "Ocorreu Um Erro Ao Enviar Sua Mensagem." 173 | 174 | #: .\termsandconditions\views.py:183 175 | msgid "Invalid Email Address." 176 | msgstr "Endereço de Email Inválido." 177 | -------------------------------------------------------------------------------- /termsandconditions/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyface/django-termsandconditions/8a2902539f51985ff81574f1f86cff8e464d6b12/termsandconditions/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /termsandconditions/locale/ru/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-03-19 21:03+0500\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=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 20 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" 21 | "%100>=11 && n%100<=14)? 2 : 3);\n" 22 | 23 | #: admin.py:13 apps.py:15 templates/termsandconditions/tc_accept_terms.html:13 24 | #: templates/termsandconditions/tc_email_terms_form.html:8 25 | #: templates/termsandconditions/tc_view_terms.html:13 26 | msgid "Terms and Conditions" 27 | msgstr "Условия и Положения" 28 | 29 | #: models.py:25 30 | msgid "IP Address" 31 | msgstr "IP Адрес" 32 | 33 | #: models.py:26 34 | msgid "Date Accepted" 35 | msgstr "Дата принятия" 36 | 37 | #: models.py:31 models.py:32 38 | msgid "User Terms and Conditions" 39 | msgstr "Пользовательское Соглашение" 40 | 41 | #: models.py:48 42 | msgid "Provide users with some info about what's changed and why" 43 | msgstr "Предоставьте пользователям некоторую информацию о том, что изменилось и почему" 44 | 45 | #: models.py:50 46 | msgid "Leave Null To Never Make Active" 47 | msgstr "Оставить Null, чтобы никогда не активировать" 48 | 49 | #: templates/termsandconditions/snippets/termsandconditions.html:16 50 | msgid "Close" 51 | msgstr "Закрыть" 52 | 53 | #: templates/termsandconditions/snippets/termsandconditions.html:29 54 | msgid "ACCEPT ALL" 55 | msgstr "Принять все" 56 | 57 | #: templates/termsandconditions/tc_accept_terms.html:6 58 | msgid "Accept Terms and Conditions" 59 | msgstr "Принять Условия и Положения" 60 | 61 | #: templates/termsandconditions/tc_accept_terms.html:16 62 | msgid "Please Accept" 63 | msgstr "Пожалуйста примите" 64 | 65 | #: templates/termsandconditions/tc_accept_terms.html:18 66 | msgid "Summary of Changes" 67 | msgstr "Сводка изменений" 68 | 69 | #: templates/termsandconditions/tc_accept_terms.html:22 70 | msgid "Full Text" 71 | msgstr "Полный текст" 72 | 73 | #: templates/termsandconditions/tc_accept_terms.html:31 74 | #: templates/termsandconditions/tc_view_terms.html:21 75 | msgid "Print" 76 | msgstr "Версия для печати" 77 | 78 | #: templates/termsandconditions/tc_accept_terms.html:37 79 | msgid "Accept" 80 | msgstr "Принять" 81 | 82 | #: templates/termsandconditions/tc_accept_terms.html:37 83 | msgid "All" 84 | msgstr "Все" 85 | 86 | #: templates/termsandconditions/tc_email_terms.html:5 87 | #: templates/termsandconditions/tc_print_terms.html:29 88 | msgid "Version" 89 | msgstr "Версия" 90 | 91 | #: templates/termsandconditions/tc_email_terms_form.html:5 92 | msgid "Email Terms and Conditions" 93 | msgstr "Отправить Условия и Положения на почту" 94 | 95 | #: templates/termsandconditions/tc_email_terms_form.html:9 96 | msgid "Email" 97 | msgstr "" 98 | 99 | #: templates/termsandconditions/tc_email_terms_form.html:12 100 | msgid "Email To:" 101 | msgstr "Отправить:" 102 | 103 | #: templates/termsandconditions/tc_email_terms_form.html:14 104 | msgid "You Requested:" 105 | msgstr "Вы Запросили:" 106 | 107 | #: templates/termsandconditions/tc_email_terms_form.html:15 108 | msgid "Send" 109 | msgstr "Отправить" 110 | 111 | #: templates/termsandconditions/tc_print_terms.html:6 112 | msgid "Print Terms and Conditions" 113 | msgstr "Напечатать Условия и Положения" 114 | 115 | #: templates/termsandconditions/tc_view_terms.html:6 116 | msgid "View Terms and Conditions" 117 | msgstr "Просмотреть Условия и Предложения" 118 | 119 | #: views.py:171 120 | msgid "Terms" 121 | msgstr "Условия" 122 | 123 | #: views.py:176 124 | msgid "Terms and Conditions Sent." 125 | msgstr "Условия и Предложения были отправлены." 126 | 127 | #: views.py:178 128 | msgid "An Error Occurred Sending Your Message." 129 | msgstr "Во Время Отправки Сообщения Возникла Ошибка." 130 | 131 | #: views.py:187 132 | msgid "Invalid Email Address." 133 | msgstr "Неверный Email Адрес." 134 | -------------------------------------------------------------------------------- /termsandconditions/management/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /termsandconditions/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /termsandconditions/management/commands/remove_old_version_acceptance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management command to remove all UserTermsAndConditions objects from the database except the latest for each terms. 3 | """ 4 | import logging 5 | from django.core.management import BaseCommand 6 | from termsandconditions.models import TermsAndConditions, UserTermsAndConditions 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Command(BaseCommand): 11 | help = "Removes evidence of acceptance of previous terms and conditions versions." 12 | 13 | def handle(self, *args, **options): 14 | active_terms = TermsAndConditions.get_active_terms_list() 15 | for term in active_terms: 16 | old_accepts = UserTermsAndConditions.objects.filter(terms__slug=term.slug).exclude(terms__pk=term.pk) 17 | logger.debug(f"About to delete these old terms accepts: {old_accepts}") 18 | old_accepts.delete() 19 | 20 | -------------------------------------------------------------------------------- /termsandconditions/middleware.py: -------------------------------------------------------------------------------- 1 | """Terms and Conditions Middleware""" 2 | from .models import TermsAndConditions 3 | from django.conf import settings 4 | import logging 5 | from .pipeline import redirect_to_terms_accept 6 | from django.utils.deprecation import MiddlewareMixin 7 | 8 | LOGGER = logging.getLogger(name="termsandconditions") 9 | 10 | ACCEPT_TERMS_PATH = getattr(settings, "ACCEPT_TERMS_PATH", "/terms/accept/") 11 | TERMS_EXCLUDE_URL_PREFIX_LIST = getattr( 12 | settings, "TERMS_EXCLUDE_URL_PREFIX_LIST", {"/admin", "/terms"} 13 | ) 14 | TERMS_EXCLUDE_URL_CONTAINS_LIST = getattr( 15 | settings, "TERMS_EXCLUDE_URL_CONTAINS_LIST", {} 16 | ) 17 | TERMS_EXCLUDE_URL_LIST = getattr( 18 | settings, 19 | "TERMS_EXCLUDE_URL_LIST", 20 | {"/", "/termsrequired/", "/logout/", "/securetoo/"}, 21 | ) 22 | 23 | 24 | class TermsAndConditionsRedirectMiddleware(MiddlewareMixin): 25 | """ 26 | This middleware checks to see if the user is logged in, and if so, 27 | if they have accepted all the active terms. 28 | """ 29 | 30 | def process_request(self, request): 31 | """Process each request to app to ensure terms have been accepted""" 32 | 33 | LOGGER.debug("termsandconditions.middleware") 34 | 35 | current_path = request.META["PATH_INFO"] 36 | 37 | if request.user.is_authenticated and is_path_protected(current_path): 38 | for term in TermsAndConditions.get_active_terms_not_agreed_to(request.user): 39 | # Check for querystring and include it if there is one 40 | qs = request.META["QUERY_STRING"] 41 | current_path += "?" + qs if qs else "" 42 | return redirect_to_terms_accept(current_path, term.slug) 43 | 44 | return None 45 | 46 | 47 | def is_path_protected(path): 48 | """ 49 | returns True if given path is to be protected, otherwise False 50 | 51 | The path is not to be protected when it appears on: 52 | TERMS_EXCLUDE_URL_PREFIX_LIST, TERMS_EXCLUDE_URL_LIST, TERMS_EXCLUDE_URL_CONTAINS_LIST or as 53 | ACCEPT_TERMS_PATH 54 | """ 55 | protected = True 56 | 57 | for exclude_path in TERMS_EXCLUDE_URL_PREFIX_LIST: 58 | if path.startswith(exclude_path): 59 | protected = False 60 | 61 | for contains_path in TERMS_EXCLUDE_URL_CONTAINS_LIST: 62 | if contains_path in path: 63 | protected = False 64 | 65 | if path in TERMS_EXCLUDE_URL_LIST: 66 | protected = False 67 | 68 | if path.startswith(ACCEPT_TERMS_PATH): 69 | protected = False 70 | 71 | return protected 72 | -------------------------------------------------------------------------------- /termsandconditions/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='TermsAndConditions', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('slug', models.SlugField(default=b'site-terms')), 20 | ('name', models.TextField(max_length=255)), 21 | ('version_number', models.DecimalField(default=1.0, max_digits=6, decimal_places=2)), 22 | ('text', models.TextField(null=True, blank=True)), 23 | ('date_active', models.DateTimeField(help_text=b'Leave Null To Never Make Active', null=True, blank=True)), 24 | ('date_created', models.DateTimeField(auto_now_add=True)), 25 | ], 26 | options={ 27 | 'ordering': ['-date_active'], 28 | 'get_latest_by': 'date_active', 29 | 'verbose_name': 'Terms and Conditions', 30 | 'verbose_name_plural': 'Terms and Conditions', 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='UserTermsAndConditions', 35 | fields=[ 36 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 37 | ('ip_address', models.GenericIPAddressField(null=True, verbose_name=b'IP Address', blank=True)), 38 | ('date_accepted', models.DateTimeField(auto_now_add=True, verbose_name=b'Date Accepted')), 39 | ('terms', models.ForeignKey(related_name='userterms', to='termsandconditions.TermsAndConditions', on_delete=models.CASCADE)), 40 | ('user', models.ForeignKey(related_name='userterms', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), 41 | ], 42 | options={ 43 | 'get_latest_by': 'date_accepted', 44 | 'verbose_name': 'User Terms and Conditions', 45 | 'verbose_name_plural': 'User Terms and Conditions', 46 | }, 47 | ), 48 | migrations.AddField( 49 | model_name='termsandconditions', 50 | name='users', 51 | field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='termsandconditions.UserTermsAndConditions', blank=True), 52 | ), 53 | migrations.AlterUniqueTogether( 54 | name='usertermsandconditions', 55 | unique_together=set([('user', 'terms')]), 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /termsandconditions/migrations/0002_termsandconditions_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('termsandconditions', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='termsandconditions', 16 | name='info', 17 | field=models.TextField( 18 | help_text=b"Provide users with some info about " 19 | b"what's changed and why", null=True, blank=True), 20 | preserve_default=True, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /termsandconditions/migrations/0003_auto_20170627_1217.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.2 on 2017-06-27 12:17 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('termsandconditions', '0002_termsandconditions_info'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='termsandconditions', 17 | name='date_active', 18 | field=models.DateTimeField(blank=True, help_text='Leave Null To Never Make Active', null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='termsandconditions', 22 | name='info', 23 | field=models.TextField(blank=True, help_text="Provide users with some info about what's changed and why", null=True), 24 | ), 25 | migrations.AlterField( 26 | model_name='termsandconditions', 27 | name='slug', 28 | field=models.SlugField(default='site-terms'), 29 | ), 30 | migrations.AlterField( 31 | model_name='usertermsandconditions', 32 | name='date_accepted', 33 | field=models.DateTimeField(auto_now_add=True, verbose_name='Date Accepted'), 34 | ), 35 | migrations.AlterField( 36 | model_name='usertermsandconditions', 37 | name='ip_address', 38 | field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address'), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /termsandconditions/migrations/0004_auto_20201107_0711.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.3 on 2020-11-07 07:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('termsandconditions', '0003_auto_20170627_1217'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='termsandconditions', 18 | name='date_active', 19 | field=models.DateTimeField(blank=True, help_text='Leave Null To Never Make Active', null=True, verbose_name='date active'), 20 | ), 21 | migrations.AlterField( 22 | model_name='termsandconditions', 23 | name='info', 24 | field=models.TextField(blank=True, default='', help_text="Provide users with some info about what's changed and why", verbose_name='info'), 25 | ), 26 | migrations.AlterField( 27 | model_name='termsandconditions', 28 | name='name', 29 | field=models.TextField(max_length=255, verbose_name='name'), 30 | ), 31 | migrations.AlterField( 32 | model_name='termsandconditions', 33 | name='text', 34 | field=models.TextField(blank=True, default='', verbose_name='text'), 35 | ), 36 | migrations.AlterField( 37 | model_name='termsandconditions', 38 | name='version_number', 39 | field=models.DecimalField(decimal_places=2, default=1.0, max_digits=6, verbose_name='version number'), 40 | ), 41 | migrations.AlterField( 42 | model_name='usertermsandconditions', 43 | name='terms', 44 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userterms', to='termsandconditions.termsandconditions', verbose_name='terms'), 45 | ), 46 | migrations.AlterField( 47 | model_name='usertermsandconditions', 48 | name='user', 49 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userterms', to=settings.AUTH_USER_MODEL, verbose_name='user'), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /termsandconditions/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyface/django-termsandconditions/8a2902539f51985ff81574f1f86cff8e464d6b12/termsandconditions/migrations/__init__.py -------------------------------------------------------------------------------- /termsandconditions/models.py: -------------------------------------------------------------------------------- 1 | """Django Models for TermsAndConditions App""" 2 | 3 | from collections import OrderedDict 4 | 5 | from django.db import models 6 | from django.conf import settings 7 | from django.core.cache import cache 8 | from django.utils import timezone 9 | from django.utils.translation import gettext_lazy as _ 10 | from django.urls import reverse 11 | 12 | import logging 13 | 14 | LOGGER = logging.getLogger(name="termsandconditions") 15 | 16 | DEFAULT_TERMS_SLUG = getattr(settings, "DEFAULT_TERMS_SLUG", "site-terms") 17 | TERMS_CACHE_SECONDS = getattr(settings, "TERMS_CACHE_SECONDS", 30) 18 | TERMS_EXCLUDE_USERS_WITH_PERM = getattr(settings, "TERMS_EXCLUDE_USERS_WITH_PERM", None) 19 | 20 | 21 | class UserTermsAndConditions(models.Model): 22 | """Holds mapping between TermsAndConditions and Users""" 23 | 24 | user = models.ForeignKey( 25 | settings.AUTH_USER_MODEL, 26 | related_name="userterms", 27 | on_delete=models.CASCADE, 28 | verbose_name=_("user"), 29 | ) 30 | terms = models.ForeignKey( 31 | "TermsAndConditions", 32 | related_name="userterms", 33 | on_delete=models.CASCADE, 34 | verbose_name=_("terms"), 35 | ) 36 | ip_address = models.GenericIPAddressField( 37 | null=True, blank=True, verbose_name=_("IP Address") 38 | ) 39 | date_accepted = models.DateTimeField( 40 | auto_now_add=True, verbose_name=_("Date Accepted") 41 | ) 42 | 43 | class Meta: 44 | """Model Meta Information""" 45 | 46 | get_latest_by = "date_accepted" 47 | verbose_name = _("User Terms and Conditions") 48 | verbose_name_plural = _("User Terms and Conditions") 49 | unique_together = ( 50 | "user", 51 | "terms", 52 | ) 53 | 54 | def __str__(self): 55 | return f"{self.user.get_username()}:{self.terms.slug}-{self.terms.version_number}" 56 | 57 | 58 | class TermsAndConditions(models.Model): 59 | """Holds Versions of TermsAndConditions 60 | Active one for a given slug is: date_active is not Null and is latest not in future""" 61 | 62 | slug = models.SlugField(default=DEFAULT_TERMS_SLUG) 63 | name = models.TextField(max_length=255, verbose_name=_("name")) 64 | users = models.ManyToManyField( 65 | settings.AUTH_USER_MODEL, through=UserTermsAndConditions, blank=True 66 | ) 67 | version_number = models.DecimalField( 68 | default=1.0, 69 | decimal_places=2, 70 | max_digits=6, 71 | verbose_name=_("version number"), 72 | ) 73 | text = models.TextField(default="", blank=True, verbose_name=_("text")) 74 | info = models.TextField( 75 | default="", 76 | blank=True, 77 | help_text=_("Provide users with some info about what's changed and why"), 78 | verbose_name=_("info"), 79 | ) 80 | date_active = models.DateTimeField( 81 | blank=True, 82 | null=True, 83 | help_text=_("Leave Null To Never Make Active"), 84 | verbose_name=_("date active"), 85 | ) 86 | date_created = models.DateTimeField(blank=True, auto_now_add=True) 87 | 88 | class Meta: 89 | """Model Meta Information""" 90 | 91 | ordering = [ 92 | "-date_active", 93 | ] 94 | get_latest_by = "date_active" 95 | verbose_name = _("Terms and Conditions") 96 | verbose_name_plural = _("Terms and Conditions") 97 | 98 | def __str__(self): # pragma: nocover 99 | return f"{self.slug}-{self.version_number:.2f}" 100 | 101 | def get_absolute_url(self): 102 | return reverse( 103 | "tc_view_specific_version_page", args=[self.slug, self.version_number] 104 | ) 105 | 106 | @staticmethod 107 | def get_active(slug=DEFAULT_TERMS_SLUG): 108 | """Finds the latest of a particular terms and conditions""" 109 | 110 | active_terms = cache.get("tandc.active_terms_" + slug) 111 | if active_terms is None: 112 | try: 113 | active_terms = TermsAndConditions.objects.filter( 114 | date_active__isnull=False, 115 | date_active__lte=timezone.now(), 116 | slug=slug, 117 | ).latest("date_active") 118 | cache.set( 119 | "tandc.active_terms_" + slug, active_terms, TERMS_CACHE_SECONDS 120 | ) 121 | except TermsAndConditions.DoesNotExist: # pragma: nocover 122 | LOGGER.error( 123 | "Requested Terms and Conditions that Have Not Been Created." 124 | ) 125 | return None 126 | 127 | return active_terms 128 | 129 | @staticmethod 130 | def get_active_terms_ids(): 131 | """Returns a list of the IDs of of all terms and conditions""" 132 | 133 | active_terms_ids = cache.get("tandc.active_terms_ids") 134 | if active_terms_ids is None: 135 | active_terms_dict = {} 136 | active_terms_ids = [] 137 | 138 | active_terms_set = TermsAndConditions.objects.filter( 139 | date_active__isnull=False, date_active__lte=timezone.now() 140 | ).order_by("date_active") 141 | for active_terms in active_terms_set: 142 | active_terms_dict[active_terms.slug] = active_terms.id 143 | 144 | active_terms_dict = OrderedDict( 145 | sorted(active_terms_dict.items(), key=lambda t: t[0]) 146 | ) 147 | 148 | for terms in active_terms_dict: 149 | active_terms_ids.append(active_terms_dict[terms]) 150 | 151 | cache.set("tandc.active_terms_ids", active_terms_ids, TERMS_CACHE_SECONDS) 152 | 153 | return active_terms_ids 154 | 155 | @staticmethod 156 | def get_active_terms_list(): 157 | """Returns all the latest active terms and conditions""" 158 | 159 | active_terms_list = cache.get("tandc.active_terms_list") 160 | if active_terms_list is None: 161 | active_terms_list = TermsAndConditions.objects.filter( 162 | id__in=TermsAndConditions.get_active_terms_ids() 163 | ).order_by("slug") 164 | cache.set("tandc.active_terms_list", active_terms_list, TERMS_CACHE_SECONDS) 165 | 166 | return active_terms_list 167 | 168 | @staticmethod 169 | def get_active_terms_not_agreed_to(user): 170 | """Checks to see if a specified user has agreed to all the latest terms and conditions""" 171 | 172 | if not user.is_authenticated: 173 | return TermsAndConditions.get_active_terms_list() 174 | 175 | if TERMS_EXCLUDE_USERS_WITH_PERM is not None: 176 | if user.has_perm(TERMS_EXCLUDE_USERS_WITH_PERM) and not user.is_superuser: 177 | # Django's has_perm() returns True if is_superuser, we don't want that 178 | return [] 179 | 180 | TERMS_EXCLUDE_SUPERUSERS = getattr(settings, "TERMS_EXCLUDE_SUPERUSERS", None) 181 | if TERMS_EXCLUDE_SUPERUSERS and user.is_superuser: 182 | return [] 183 | 184 | not_agreed_terms = cache.get("tandc.not_agreed_terms_" + user.get_username()) 185 | if not_agreed_terms is None: 186 | try: 187 | LOGGER.debug("Not Agreed Terms") 188 | not_agreed_terms = ( 189 | TermsAndConditions.get_active_terms_list() 190 | .exclude( 191 | userterms__in=UserTermsAndConditions.objects.filter(user=user) 192 | ) 193 | .order_by("slug") 194 | ) 195 | 196 | cache.set( 197 | "tandc.not_agreed_terms_" + user.get_username(), 198 | not_agreed_terms, 199 | TERMS_CACHE_SECONDS, 200 | ) 201 | except (TypeError, UserTermsAndConditions.DoesNotExist): 202 | return [] 203 | 204 | return not_agreed_terms 205 | -------------------------------------------------------------------------------- /termsandconditions/pipeline.py: -------------------------------------------------------------------------------- 1 | """This file contains functions used as part of a user creation pipeline, such as django-social-auth.""" 2 | 3 | from urllib.parse import urlunparse, urlparse 4 | 5 | from .models import TermsAndConditions 6 | from django.http import HttpResponseRedirect, QueryDict 7 | from django.conf import settings 8 | import logging 9 | 10 | ACCEPT_TERMS_PATH = getattr(settings, "ACCEPT_TERMS_PATH", "/terms/accept/") 11 | TERMS_RETURNTO_PARAM = getattr(settings, "TERMS_RETURNTO_PARAM", "returnTo") 12 | 13 | LOGGER = logging.getLogger(name="termsandconditions") 14 | 15 | 16 | def user_accept_terms(backend, user, uid, social_user=None, *args, **kwargs): 17 | """Check if the user has accepted the terms and conditions after creation.""" 18 | 19 | LOGGER.debug("user_accept_terms") 20 | 21 | if TermsAndConditions.get_active_terms_not_agreed_to(user): 22 | return redirect_to_terms_accept("/") 23 | else: 24 | return {"social_user": social_user, "user": user} 25 | 26 | 27 | def redirect_to_terms_accept(current_path="/", slug="default"): 28 | """Redirect the user to the terms and conditions accept page.""" 29 | redirect_url_parts = list(urlparse(ACCEPT_TERMS_PATH)) 30 | if slug != "default": 31 | redirect_url_parts[2] += slug + "/" 32 | querystring = QueryDict(redirect_url_parts[4], mutable=True) 33 | querystring[TERMS_RETURNTO_PARAM] = current_path 34 | redirect_url_parts[4] = querystring.urlencode(safe="/") 35 | return HttpResponseRedirect(urlunparse(redirect_url_parts)) 36 | -------------------------------------------------------------------------------- /termsandconditions/signals.py: -------------------------------------------------------------------------------- 1 | """ Signals for Django """ 2 | 3 | import logging 4 | from django.core.cache import cache 5 | from django.dispatch import receiver 6 | from .models import TermsAndConditions, UserTermsAndConditions 7 | from django.db.models.signals import post_delete, post_save 8 | 9 | LOGGER = logging.getLogger(name="termsandconditions") 10 | 11 | 12 | @receiver([post_delete, post_save], sender=UserTermsAndConditions) 13 | def user_terms_updated(sender, **kwargs): 14 | """Called when user terms and conditions is changed - to force cache clearing""" 15 | LOGGER.debug("User T&C Updated Signal Handler") 16 | if kwargs.get("instance").user: 17 | cache.delete( 18 | "tandc.not_agreed_terms_" + kwargs.get("instance").user.get_username() 19 | ) 20 | 21 | 22 | @receiver([post_delete, post_save], sender=TermsAndConditions) 23 | def terms_updated(sender, **kwargs): 24 | """Called when terms and conditions is changed - to force cache clearing""" 25 | LOGGER.debug("T&C Updated Signal Handler") 26 | cache.delete("tandc.active_terms_ids") 27 | cache.delete("tandc.active_terms_list") 28 | if kwargs.get("instance").slug: 29 | cache.delete("tandc.active_terms_" + kwargs.get("instance").slug) 30 | for utandc in UserTermsAndConditions.objects.all(): 31 | cache.delete("tandc.not_agreed_terms_" + utandc.user.get_username()) 32 | -------------------------------------------------------------------------------- /termsandconditions/static/termsandconditions/css/modal.css: -------------------------------------------------------------------------------- 1 | #termsandconditions { 2 | position: fixed; 3 | bottom: 0; 4 | left: 50%; 5 | width: 500px; 6 | margin-left: -250px; 7 | z-index: 1000; 8 | background: #ffffff; 9 | box-shadow: 0 -1px 10px 1px rgba(0, 0, 0, 0.3); 10 | } 11 | 12 | #termsandconditions #toc-content { 13 | margin: 0 15px; 14 | } 15 | 16 | #termsandconditions #toc-header { 17 | position: relative; 18 | background-color: #0a62a9; 19 | color: #ffffff; 20 | margin: 0; 21 | padding: 12px 15px; 22 | } 23 | 24 | #termsandconditions #toc-header h6 { 25 | color: #ffffff; 26 | margin: 0; 27 | font-size: 18px; 28 | font-weight: 400; 29 | } 30 | 31 | #termsandconditions #toc-header .toc-close { 32 | position: absolute; 33 | top: 50%; 34 | margin-top: -14px; 35 | right: 15px; 36 | color: #ffffff; 37 | font-size: 20px; 38 | font-weight: 600; 39 | } 40 | 41 | #termsandconditions #toc-header .toc-close:hover { 42 | color: #d3d3d3; 43 | text-decoration: none; 44 | } 45 | 46 | #termsandconditions #toc-content #toc-body { 47 | padding: 15px 0; 48 | } 49 | 50 | #termsandconditions #toc-content #toc-body a:hover { 51 | text-decoration: underline; 52 | } 53 | 54 | #termsandconditions #toc-content #toc-footer { 55 | width: 100%; 56 | border-top: 1px #d3d3d3 solid; 57 | } 58 | 59 | #termsandconditions #toc-content #toc-footer .toc-accept-all-btn { 60 | float: right; 61 | margin: 10px 0; 62 | font-weight: 600; 63 | color: #0a62a9; 64 | padding: 8px 10px; 65 | border-radius: 5px; 66 | } 67 | 68 | #termsandconditions #toc-content #toc-footer .toc-accept-all-btn:hover { 69 | background-color: #d3d3d3; 70 | text-decoration: none; 71 | } 72 | 73 | @media(max-width: 500px) { 74 | #termsandconditions { 75 | width: 100%; 76 | margin-left: 0; 77 | left: 0; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /termsandconditions/static/termsandconditions/css/view_accept.css: -------------------------------------------------------------------------------- 1 | .toc-container { 2 | border: 2px solid lightgray; 3 | padding: 10px 20px; 4 | max-height: 400px; 5 | overflow: auto; 6 | margin-bottom: 20px; 7 | } 8 | 9 | .toc-print-container { 10 | margin: 0 50px; 11 | } -------------------------------------------------------------------------------- /termsandconditions/static/termsandconditions/js/modal.js: -------------------------------------------------------------------------------- 1 | function termsandconditions_overlay() { 2 | var el = document.getElementsByClassName("termsandconditions-modal"); 3 | var i; 4 | for (i = 0; i < el.length; i++) { 5 | el[i].style.visibility = (el[i].style.visibility == "visible") ? "hidden" : "visible"; 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /termsandconditions/templates/termsandconditions/snippets/termsandconditions.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load i18n %} 3 | 4 | {% if not_agreed_terms %} 5 | 6 | 7 | 12 | 13 |
14 |
15 |
{% trans "Our Terms and Conditions Have Changed" %}
16 | × 17 |
18 |
19 |
20 | Changes have been made to our 21 | {% for terms in not_agreed_terms %} 22 | {% if not forloop.first and not forloop.last %}, {% endif %} 23 | {% if forloop.last and not_agreed_terms|length > 2 %}, and {% elif forloop.last and not_agreed_terms|length > 1 %} and {% endif %} 24 | {% if terms.name %}{{ terms.name|safe }}{% endif %} 25 | {% endfor %} 26 | document{{ not_agreed_terms|length|pluralize }}. {% trans "Please, review and accept the changes to prevent future interruptions." %} 27 |
28 | 31 |
32 |
33 | {% endif %} 34 | 35 | -------------------------------------------------------------------------------- /termsandconditions/templates/termsandconditions/tc_accept_terms.html: -------------------------------------------------------------------------------- 1 | {% extends terms_base_template %} 2 | 3 | {% load static %} 4 | {% load i18n %} 5 | 6 | {% block title %}{% trans 'Accept Terms and Conditions' %}{% endblock %} 7 | {% block styles %} 8 | {{ block.super }} 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 | {{ form.errors }} 15 | {% for terms in form.initial.terms %} 16 |

{% trans 'Please Accept' %} {{ terms.name|safe }} {{ terms.version_number|safe }}

17 | {% if terms.info %} 18 |

{% trans 'Summary of Changes' %}

19 |
20 | {{ terms.info|safe }} 21 |
22 |

{% trans 'Full Text' %}

23 | {% endif %} 24 |
25 |
26 | {{ terms.text|safe }} 27 |
28 |
29 | 30 |

31 | {% trans 'Print' %} {{ terms.name|safe }} 33 |

34 | {% endfor %} 35 |
36 | {% csrf_token %} 37 | {{ form.terms }} 38 | {{ form.returnTo }} 39 |

40 |
41 |
42 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions/templates/termsandconditions/tc_email_terms.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {{ terms.name|safe }} 4 | 5 | {% trans 'Version' %} {{ terms.version_number|safe }} 6 | 7 | {{ terms.text|safe }} -------------------------------------------------------------------------------- /termsandconditions/templates/termsandconditions/tc_email_terms_form.html: -------------------------------------------------------------------------------- 1 | {% extends terms_base_template %} 2 | 3 | {% load i18n %} 4 | 5 | {% block headtitle %}{% trans 'Email Terms and Conditions' %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{% trans 'Email' %} {{ form.initial.terms.name|safe }}

10 |
11 | {% csrf_token %} 12 | {{ form.email_address }} 13 | {{ form.terms }} 14 | 15 |

16 |
17 |
18 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions/templates/termsandconditions/tc_print_terms.html: -------------------------------------------------------------------------------- 1 | {% extends terms_base_template %} 2 | 3 | {% load static %} 4 | {% load i18n %} 5 | 6 | {% block title %}{% trans 'Print Terms and Conditions' %}{% endblock %} 7 | 8 | {% block body_class %}{% endblock %} 9 | 10 | {% block styles %} 11 | {{ block.super }} 12 | 13 | {% endblock %} 14 | 15 | {% block head %} 16 | 21 | {% endblock %} 22 | 23 | {% block header %}{% endblock %} 24 | 25 | {% block content %} 26 | {% for terms in terms_list %} 27 |
28 |

{{ terms.name|safe }}

29 |

{% trans 'Version' %} {{ terms.version_number|safe }}

30 |
31 | {{ terms.text|safe }} 32 |
33 |
34 | {% endfor %} 35 | {% endblock %} 36 | 37 | {% block footer %}{% endblock %} -------------------------------------------------------------------------------- /termsandconditions/templates/termsandconditions/tc_view_terms.html: -------------------------------------------------------------------------------- 1 | {% extends terms_base_template %} 2 | 3 | {% load static %} 4 | {% load i18n %} 5 | 6 | {% block title %}{% trans 'View Terms and Conditions' %}{% endblock %} 7 | {% block styles %} 8 | {{ block.super }} 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 | {% for terms in terms_list %} 15 | {% if terms %} 16 |

{{ terms.name|safe }} {{ terms.version_number|safe }}

17 | 18 |
19 | {{ terms.text|safe }} 20 |
21 |

{% trans 'Print' %} {{ terms.name|safe }}

23 | {% else %} 24 |

{% trans "No terms defined." %}

25 | {% endif %} 26 | {% empty %} 27 |

{% trans "No terms for you to accept." %}

28 | {% endfor %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /termsandconditions/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /termsandconditions/templatetags/terms_tags.py: -------------------------------------------------------------------------------- 1 | """Django Tags""" 2 | from urllib.parse import urlparse 3 | 4 | from django import template 5 | from ..models import TermsAndConditions 6 | from ..middleware import is_path_protected 7 | from django.conf import settings 8 | 9 | register = template.Library() 10 | DEFAULT_HTTP_PATH_FIELD = "PATH_INFO" 11 | TERMS_HTTP_PATH_FIELD = getattr( 12 | settings, "TERMS_HTTP_PATH_FIELD", DEFAULT_HTTP_PATH_FIELD 13 | ) 14 | 15 | 16 | @register.inclusion_tag( 17 | "termsandconditions/snippets/termsandconditions.html", takes_context=True 18 | ) 19 | def show_terms_if_not_agreed(context, field=TERMS_HTTP_PATH_FIELD): 20 | """Displays a modal on a current page if a user has not yet agreed to the 21 | given terms. If terms are not specified, the default slug is used. 22 | 23 | A small snippet is included into your template if a user 24 | who requested the view has not yet agreed the terms. The snippet takes 25 | care of displaying a respective modal. 26 | """ 27 | request = context["request"] 28 | url = urlparse(request.META[field]) 29 | not_agreed_terms = TermsAndConditions.get_active_terms_not_agreed_to(request.user) 30 | 31 | if not_agreed_terms and is_path_protected(url.path): 32 | return {"not_agreed_terms": not_agreed_terms, "returnTo": url.path} 33 | else: 34 | return {"not_agreed_terms": False} 35 | 36 | 37 | @register.filter 38 | def as_template(obj): 39 | """Converts objects to a Template instance 40 | 41 | This is useful in cases when a template variable contains html-like text, 42 | which includes also django template language tags and should be rendered. 43 | 44 | For instance, in case of termsandconditions object, its text field may 45 | include a string such as `My url`, 46 | which should be properly rendered. 47 | 48 | To achieve this goal, one can use template `include` with `as_template` 49 | filter: 50 | ... 51 | {% include your_variable|as_template %} 52 | ... 53 | """ 54 | return template.Template(obj) 55 | -------------------------------------------------------------------------------- /termsandconditions/tests.py: -------------------------------------------------------------------------------- 1 | """Unit Tests for the termsandconditions module""" 2 | 3 | from importlib import import_module 4 | import logging 5 | 6 | from django.core import mail 7 | from django.core.cache import cache 8 | from django.http import HttpResponseRedirect 9 | from django.conf import settings 10 | from django.test import TestCase, RequestFactory 11 | from django.contrib.auth.models import User, ContentType, Permission 12 | from django.template import Context, Template 13 | 14 | from .models import TermsAndConditions, UserTermsAndConditions, DEFAULT_TERMS_SLUG 15 | from .pipeline import user_accept_terms 16 | from .templatetags.terms_tags import show_terms_if_not_agreed 17 | 18 | LOGGER = logging.getLogger(name="termsandconditions") 19 | 20 | 21 | class TermsAndConditionsTests(TestCase): 22 | """Tests Terms and Conditions Module""" 23 | 24 | def setUp(self): 25 | """Setup for each test""" 26 | LOGGER.debug("Test Setup") 27 | 28 | self.su = User.objects.create_superuser("su", "su@example.com", "superstrong") 29 | self.user1 = User.objects.create_user( 30 | "user1", "user1@user1.com", "user1password" 31 | ) 32 | self.user2 = User.objects.create_user( 33 | "user2", "user2@user2.com", "user2password" 34 | ) 35 | self.user3 = User.objects.create_user( 36 | "user3", "user3@user3.com", "user3password" 37 | ) 38 | self.terms1 = TermsAndConditions.objects.create( 39 | id=1, 40 | slug="site-terms", 41 | name="Site Terms", 42 | text="Site Terms and Conditions 1", 43 | version_number=1.0, 44 | date_active="2012-01-01", 45 | ) 46 | self.terms2 = TermsAndConditions.objects.create( 47 | id=2, 48 | slug="site-terms", 49 | name="Site Terms", 50 | text="Site Terms and Conditions 2", 51 | version_number=2.0, 52 | date_active="2012-01-05", 53 | ) 54 | self.terms3 = TermsAndConditions.objects.create( 55 | id=3, 56 | slug="contrib-terms", 57 | name="Contributor Terms", 58 | text="Contributor Terms and Conditions 1.5", 59 | version_number=1.5, 60 | date_active="2012-01-01", 61 | ) 62 | self.terms4 = TermsAndConditions.objects.create( 63 | id=4, 64 | slug="contrib-terms", 65 | name="Contributor Terms", 66 | text="Contributor Terms and Conditions 2", 67 | version_number=2.0, 68 | date_active="2100-01-01", 69 | ) 70 | 71 | # give user3 permission to skip T&Cs 72 | content_type = ContentType.objects.get_for_model(type(self.user3)) 73 | self.skip_perm = Permission.objects.create( 74 | content_type=content_type, name="Can skip T&Cs", codename="can_skip_t&c" 75 | ) 76 | self.user3.user_permissions.add(self.skip_perm) 77 | 78 | def tearDown(self): 79 | """Teardown for each test""" 80 | LOGGER.debug("Test TearDown") 81 | User.objects.all().delete() 82 | TermsAndConditions.objects.all().delete() 83 | UserTermsAndConditions.objects.all().delete() 84 | 85 | def test_social_redirect(self): 86 | """Test the agreed_to_terms redirect from social pipeline""" 87 | LOGGER.debug("Test the social pipeline") 88 | response = user_accept_terms("backend", self.user1, "123") 89 | self.assertIsInstance(response, HttpResponseRedirect) 90 | 91 | # Accept the terms and try again 92 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2) 93 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms3) 94 | response = user_accept_terms("backend", self.user1, "123") 95 | self.assertIsInstance(response, dict) 96 | 97 | def test_get_active_terms_list(self): 98 | """Test get list of active T&Cs""" 99 | active_list = TermsAndConditions.get_active_terms_list() 100 | self.assertEqual(2, len(active_list)) 101 | self.assertQuerySetEqual(active_list, [self.terms3, self.terms2]) 102 | 103 | def test_get_active_terms_not_agreed_to(self): 104 | """Test get T&Cs not agreed to""" 105 | active_list = TermsAndConditions.get_active_terms_not_agreed_to(self.user1) 106 | self.assertEqual(2, len(active_list)) 107 | self.assertQuerySetEqual(active_list, [self.terms3, self.terms2]) 108 | 109 | def test_user_is_excluded(self): 110 | """Test user3 has perm which excludes them from having to accept T&Cs""" 111 | active_list = TermsAndConditions.get_active_terms_not_agreed_to(self.user3) 112 | self.assertEqual([], active_list) 113 | 114 | def test_superuser_is_not_implicitly_excluded(self): 115 | """Test su should have to accept T&Cs even if they are superuser but don't explicitly have the skip perm""" 116 | active_list = TermsAndConditions.get_active_terms_not_agreed_to(self.su) 117 | self.assertEqual(2, len(active_list)) 118 | self.assertQuerySetEqual(active_list, [self.terms3, self.terms2]) 119 | 120 | def test_superuser_cannot_skip(self): 121 | """Test su still has to accept even if they are explicitly given the skip perm""" 122 | self.su.user_permissions.add(self.skip_perm) 123 | active_list = TermsAndConditions.get_active_terms_not_agreed_to(self.su) 124 | self.assertEqual(2, len(active_list)) 125 | self.assertQuerySetEqual(active_list, [self.terms3, self.terms2]) 126 | 127 | def test_superuser_excluded(self): 128 | """Test su doesn't have to accept with TERMS_EXCLUDE_SUPERUSERS set""" 129 | with self.settings(TERMS_EXCLUDE_SUPERUSERS=True): 130 | active_list = TermsAndConditions.get_active_terms_not_agreed_to(self.su) 131 | self.assertEqual([], active_list) 132 | 133 | def test_get_active_terms_ids(self): 134 | """Test get ids of active T&Cs""" 135 | active_list = TermsAndConditions.get_active_terms_ids() 136 | self.assertEqual(2, len(active_list)) 137 | self.assertEqual(active_list, [3, 2]) 138 | 139 | def test_terms_and_conditions_models(self): 140 | """Various tests of the TermsAndConditions Module""" 141 | 142 | # Testing Direct Assignment of Acceptance 143 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms1) 144 | UserTermsAndConditions.objects.create(user=self.user2, terms=self.terms3) 145 | 146 | self.assertEqual(1.0, self.user1.userterms.get().terms.version_number) 147 | self.assertEqual(1.5, self.user2.userterms.get().terms.version_number) 148 | 149 | self.assertEqual("user1", self.terms1.users.all()[0].get_username()) 150 | 151 | # Testing the get_active static method of TermsAndConditions 152 | self.assertEqual( 153 | 2.0, TermsAndConditions.get_active(slug="site-terms").version_number 154 | ) 155 | self.assertEqual( 156 | 1.5, TermsAndConditions.get_active(slug="contrib-terms").version_number 157 | ) 158 | 159 | # Testing the unicode method of TermsAndConditions 160 | self.assertEqual( 161 | "site-terms-2.00", str(TermsAndConditions.get_active(slug="site-terms")) 162 | ) 163 | self.assertEqual( 164 | "contrib-terms-1.50", 165 | str(TermsAndConditions.get_active(slug="contrib-terms")), 166 | ) 167 | 168 | def test_middleware_redirect(self): 169 | """Validate that a user is redirected to the terms accept page if they are logged in, and decorator is on method""" 170 | 171 | UserTermsAndConditions.objects.all().delete() 172 | 173 | LOGGER.debug("Test user1 login for middleware") 174 | login_response = self.client.login(username="user1", password="user1password") 175 | self.assertTrue(login_response) 176 | 177 | LOGGER.debug("Test /secure/ after login") 178 | logged_in_response = self.client.get("/secure/", follow=True) 179 | self.assertRedirects( 180 | logged_in_response, "/terms/accept/contrib-terms/?returnTo=/secure/" 181 | ) 182 | 183 | def test_terms_required_redirect(self): 184 | """Validate that a user is redirected to the terms accept page if logged in, and decorator is on method""" 185 | 186 | LOGGER.debug("Test /termsrequired/ pre login") 187 | not_logged_in_response = self.client.get("/termsrequired/", follow=True) 188 | self.assertRedirects( 189 | not_logged_in_response, "/accounts/login/?next=/termsrequired/" 190 | ) 191 | 192 | LOGGER.debug("Test user1 login") 193 | login_response = self.client.login(username="user1", password="user1password") 194 | self.assertTrue(login_response) 195 | 196 | LOGGER.debug("Test /termsrequired/ after login") 197 | logged_in_response = self.client.get("/termsrequired/", follow=True) 198 | self.assertRedirects( 199 | logged_in_response, "/terms/accept/?returnTo=/termsrequired/" 200 | ) 201 | 202 | LOGGER.debug("Test no redirect for /termsrequired/ after accept") 203 | accepted_response = self.client.post( 204 | "/terms/accept/", {"terms": 2, "returnTo": "/termsrequired/"}, follow=True 205 | ) 206 | self.assertContains(accepted_response, "Please Accept") 207 | 208 | LOGGER.debug("Test response after termsrequired accept") 209 | terms_required_response = self.client.get("/termsrequired/", follow=True) 210 | self.assertContains(terms_required_response, "Please Accept") 211 | 212 | def test_accept(self): 213 | """Validate that accepting terms works""" 214 | 215 | LOGGER.debug("Test user1 login for accept") 216 | login_response = self.client.login(username="user1", password="user1password") 217 | self.assertTrue(login_response) 218 | 219 | LOGGER.debug("Test /terms/accept/ get") 220 | accept_response = self.client.get("/terms/accept/", follow=True) 221 | self.assertContains(accept_response, "Accept") 222 | 223 | LOGGER.debug("Test /terms/accept/ post") 224 | chained_terms_response = self.client.post( 225 | "/terms/accept/", {"terms": 2, "returnTo": "/secure/"}, follow=True 226 | ) 227 | self.assertContains(chained_terms_response, "Contributor") 228 | 229 | LOGGER.debug("Test /terms/accept/contrib-terms/1.5/ post") 230 | accept_version_response = self.client.get( 231 | "/terms/accept/contrib-terms/1.5/", follow=True 232 | ) 233 | self.assertContains( 234 | accept_version_response, "Contributor Terms and Conditions 1.5" 235 | ) 236 | 237 | LOGGER.debug("Test /terms/accept/contrib-terms/3/ post") 238 | accept_version_post_response = self.client.post( 239 | "/terms/accept/", {"terms": 3, "returnTo": "/secure/"}, follow=True 240 | ) 241 | self.assertContains(accept_version_post_response, "Secure") 242 | 243 | def _post_accept(self, return_to): 244 | # Pre-accept terms 2 and 3 245 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2) 246 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms3) 247 | 248 | LOGGER.debug("Test user1 login for test_accept_redirect") 249 | login_response = self.client.login(username="user1", password="user1password") 250 | self.assertTrue(login_response) 251 | 252 | LOGGER.debug("Test /terms/accept/site-terms/1/ post") 253 | accept_response = self.client.post( 254 | "/terms/accept/", {"terms": 1, "returnTo": return_to}, follow=True 255 | ) 256 | return accept_response 257 | 258 | def test_accept_redirect_safe(self): 259 | accept_response = self._post_accept("/secure/") 260 | self.assertRedirects(accept_response, "/secure/") 261 | 262 | def test_accept_redirect_unsafe(self): 263 | accept_response = self._post_accept("http://attacker/") 264 | self.assertRedirects(accept_response, "/") 265 | 266 | def test_accept_redirect_unsafe_2(self): 267 | accept_response = self._post_accept("//attacker.com") 268 | self.assertRedirects(accept_response, "/") 269 | 270 | def test_accept_redirect_unsafe_3(self): 271 | accept_response = self._post_accept("///attacker.com") 272 | self.assertRedirects(accept_response, "/") 273 | 274 | def test_accept_redirect_unsafe_4(self): 275 | accept_response = self._post_accept("////attacker.com") 276 | self.assertRedirects(accept_response, "/") 277 | 278 | def test_accept_store_ip_address(self): 279 | """Test with IP address storage setting true (default)""" 280 | self.client.login(username="user1", password="user1password") 281 | self.client.post( 282 | "/terms/accept/", {"terms": 2, "returnTo": "/secure/"}, follow=True 283 | ) 284 | user_terms = UserTermsAndConditions.objects.all()[0] 285 | self.assertEqual(user_terms.user, self.user1) 286 | self.assertEqual(user_terms.terms, self.terms2) 287 | self.assertTrue(user_terms.ip_address) 288 | 289 | def test_accept_store_ip_address_multiple(self): 290 | """Test storing IP address when it is a list""" 291 | self.client.login(username="user1", password="user1password") 292 | self.client.post( 293 | "/terms/accept/", 294 | {"terms": 2, "returnTo": "/secure/"}, 295 | follow=True, 296 | REMOTE_ADDR="0.0.0.0, 1.1.1.1", 297 | ) 298 | user_terms = UserTermsAndConditions.objects.all()[0] 299 | self.assertEqual(user_terms.user, self.user1) 300 | self.assertEqual(user_terms.terms, self.terms2) 301 | self.assertTrue(user_terms.ip_address) 302 | 303 | def test_accept_no_ip_address(self): 304 | """Test with IP address storage setting false""" 305 | self.client.login(username="user1", password="user1password") 306 | with self.settings(TERMS_STORE_IP_ADDRESS=False): 307 | self.client.post( 308 | "/terms/accept/", {"terms": 2, "returnTo": "/secure/"}, follow=True 309 | ) 310 | user_terms = UserTermsAndConditions.objects.all()[0] 311 | self.assertFalse(user_terms.ip_address) 312 | 313 | def test_terms_upgrade(self): 314 | """Validate a user is prompted to accept terms again when new version comes out""" 315 | 316 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2) 317 | 318 | LOGGER.debug("Test user1 login pre upgrade") 319 | login_response = self.client.login(username="user1", password="user1password") 320 | self.assertTrue(login_response) 321 | 322 | LOGGER.debug("Test user1 not redirected after login") 323 | logged_in_response = self.client.get("/secure/", follow=True) 324 | self.assertContains(logged_in_response, "Contributor") 325 | 326 | # First, Accept Contributor Terms 327 | LOGGER.debug("Test /terms/accept/contrib-terms/3/ post") 328 | self.client.post( 329 | "/terms/accept/", {"terms": 3, "returnTo": "/secure/"}, follow=True 330 | ) 331 | 332 | LOGGER.debug("Test upgrade terms") 333 | self.terms5 = TermsAndConditions.objects.create( 334 | id=5, 335 | slug="site-terms", 336 | name="Site Terms", 337 | text="Terms and Conditions2", 338 | version_number=2.5, 339 | date_active="2012-02-05", 340 | ) 341 | 342 | LOGGER.debug("Test user1 is redirected when changing pages") 343 | post_upgrade_response = self.client.get("/secure/", follow=True) 344 | self.assertRedirects( 345 | post_upgrade_response, "/terms/accept/site-terms/?returnTo=/secure/" 346 | ) 347 | 348 | def test_no_middleware(self): 349 | """Test a secure page with the middleware excepting it""" 350 | 351 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms2) 352 | 353 | LOGGER.debug("Test user1 login no middleware") 354 | login_response = self.client.login(username="user1", password="user1password") 355 | self.assertTrue(login_response) 356 | 357 | LOGGER.debug("Test user1 not redirected after login") 358 | logged_in_response = self.client.get("/securetoo/", follow=True) 359 | self.assertContains(logged_in_response, "SECOND") 360 | 361 | LOGGER.debug("Test startswith '/admin' pages not redirecting") 362 | admin_response = self.client.get("/admin", follow=True) 363 | self.assertContains(admin_response, "administration") 364 | 365 | def test_anonymous_terms_view(self): 366 | """Test Accessing the View Terms and Conditions for Anonymous User""" 367 | active_terms = TermsAndConditions.get_active_terms_list() 368 | 369 | LOGGER.debug("Test /terms/ with anon") 370 | root_response = self.client.get("/terms/", follow=True) 371 | for terms in active_terms: 372 | self.assertContains(root_response, terms.name) 373 | self.assertContains(root_response, terms.text) 374 | self.assertContains(root_response, "Terms and Conditions") 375 | 376 | LOGGER.debug("Test /terms/view/site-terms with anon") 377 | slug_response = self.client.get(self.terms2.get_absolute_url(), follow=True) 378 | self.assertContains(slug_response, self.terms2.name) 379 | self.assertContains(slug_response, self.terms2.text) 380 | self.assertContains(slug_response, "Terms and Conditions") 381 | 382 | LOGGER.debug("Test /terms/view/contributor-terms/1.5 with anon") 383 | version_response = self.client.get(self.terms3.get_absolute_url(), follow=True) 384 | self.assertContains(version_response, self.terms3.name) 385 | self.assertContains(version_response, self.terms3.text) 386 | 387 | def test_user_terms_view(self): 388 | """Test Accessing the View Terms and Conditions Page for Logged In User""" 389 | login_response = self.client.login(username="user1", password="user1password") 390 | self.assertTrue(login_response) 391 | 392 | user1_not_agreed_terms = TermsAndConditions.get_active_terms_not_agreed_to( 393 | self.user1 394 | ) 395 | self.assertEqual(len(user1_not_agreed_terms), 2) 396 | 397 | LOGGER.debug("Test /terms/ with user1") 398 | root_response = self.client.get("/terms/", follow=True) 399 | for terms in user1_not_agreed_terms: 400 | self.assertContains(root_response, terms.text) 401 | self.assertContains(root_response, "Terms and Conditions") 402 | self.assertContains(root_response, "Sign Out") 403 | 404 | # Accept terms and check again 405 | UserTermsAndConditions.objects.create(user=self.user1, terms=self.terms3) 406 | user1_not_agreed_terms = TermsAndConditions.get_active_terms_not_agreed_to( 407 | self.user1 408 | ) 409 | self.assertEqual(len(user1_not_agreed_terms), 1) 410 | LOGGER.debug("Test /terms/ with user1 after accept") 411 | post_accept_response = self.client.get("/terms/", follow=True) 412 | for terms in user1_not_agreed_terms: 413 | self.assertContains(post_accept_response, terms.text) 414 | self.assertNotContains(post_accept_response, self.terms3.name) 415 | self.assertContains(post_accept_response, "Terms and Conditions") 416 | self.assertContains(post_accept_response, "Sign Out") 417 | 418 | # Check by slug and version while logged in 419 | LOGGER.debug("Test /terms/view/site-terms as user1") 420 | slug_response = self.client.get(self.terms2.get_absolute_url(), follow=True) 421 | self.assertContains(slug_response, self.terms2.name) 422 | self.assertContains(slug_response, self.terms2.text) 423 | self.assertContains(slug_response, "Terms and Conditions") 424 | self.assertContains(slug_response, "Sign Out") 425 | 426 | LOGGER.debug("Test /terms/view/site-terms/1.5 as user1") 427 | version_response = self.client.get(self.terms3.get_absolute_url(), follow=True) 428 | self.assertContains(version_response, self.terms3.name) 429 | self.assertContains(version_response, self.terms3.text) 430 | self.assertContains(version_response, "Terms and Conditions") 431 | self.assertContains(slug_response, "Sign Out") 432 | 433 | def test_user_pipeline(self): 434 | """Test the case of a user being partially created via the django-socialauth pipeline""" 435 | 436 | LOGGER.debug("Test /terms/accept/ post for no user") 437 | no_user_response = self.client.post("/terms/accept/", {"terms": 2}, follow=True) 438 | self.assertContains(no_user_response, "Home") 439 | 440 | user = {"pk": self.user1.id} 441 | kwa = {"user": user} 442 | partial_pipeline = {"kwargs": kwa} 443 | 444 | engine = import_module(settings.SESSION_ENGINE) 445 | store = engine.SessionStore() 446 | store.save() 447 | self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key 448 | 449 | session = self.client.session 450 | session["partial_pipeline"] = partial_pipeline 451 | session.save() 452 | 453 | self.assertTrue("partial_pipeline" in self.client.session) 454 | 455 | LOGGER.debug("Test /terms/accept/ post for pipeline user") 456 | pipeline_response = self.client.post( 457 | "/terms/accept/", {"terms": 2, "returnTo": "/anon"}, follow=True 458 | ) 459 | self.assertContains(pipeline_response, "Anon") 460 | 461 | def test_email_terms(self): 462 | """Test emailing terms and conditions""" 463 | LOGGER.debug("Test /terms/email/") 464 | email_form_response = self.client.get("/terms/email/", follow=True) 465 | self.assertContains(email_form_response, "Email") 466 | 467 | LOGGER.debug("Test /terms/email/ post, expecting email fail") 468 | email_send_response = self.client.post( 469 | "/terms/email/", 470 | { 471 | "email_address": "foo@foo.com", 472 | "email_subject": "Terms Email", 473 | "terms": 2, 474 | "returnTo": "/", 475 | }, 476 | follow=True, 477 | ) 478 | self.assertEqual( 479 | len(mail.outbox), 1 480 | ) # Check that there is one email in the test outbox 481 | self.assertContains(email_send_response, "Sent") 482 | 483 | LOGGER.debug("Test /terms/email/ post, expecting email fail") 484 | email_fail_response = self.client.post( 485 | "/terms/email/", 486 | { 487 | "email_address": "INVALID EMAIL ADDRESS", 488 | "email_subject": "Terms Email", 489 | "terms": 2, 490 | "returnTo": "/", 491 | }, 492 | follow=True, 493 | ) 494 | self.assertContains(email_fail_response, "Invalid") 495 | 496 | 497 | class TermsAndConditionsTemplateTagsTestCase(TestCase): 498 | """Tests Tags for T&C""" 499 | 500 | def setUp(self): 501 | """Setup for each test""" 502 | self.user1 = User.objects.create_user( 503 | "user1", "user1@user1.com", "user1password" 504 | ) 505 | self.template_string_1 = ( 506 | "{% load terms_tags %}" "{% show_terms_if_not_agreed %}" 507 | ) 508 | self.template_string_2 = ( 509 | "{% load terms_tags %}" 510 | '{% show_terms_if_not_agreed slug="specific-terms" %}' 511 | ) 512 | self.template_string_3 = ( 513 | "{% load terms_tags %}" "{% include terms.text|as_template %}" 514 | ) 515 | self.terms1 = TermsAndConditions.objects.create( 516 | id=1, 517 | slug="site-terms", 518 | name="Site Terms", 519 | text="Site Terms and Conditions 1", 520 | version_number=1.0, 521 | date_active="2012-01-01", 522 | ) 523 | cache.clear() 524 | 525 | def _make_context(self, url): 526 | """Build Up Context - Used in many tests""" 527 | context = {} 528 | context["request"] = RequestFactory() 529 | context["request"].user = self.user1 530 | context["request"].META = {"PATH_INFO": url} 531 | return context 532 | 533 | def render_template(self, string, context=None): 534 | """a helper method to render simplistic test templates""" 535 | request = RequestFactory().get("/test") 536 | request.user = self.user1 537 | request.context = context or {} 538 | return Template(string).render(Context({"request": request})) 539 | 540 | def test_show_terms_if_not_agreed(self): 541 | """test if show_terms_if_not_agreed template tag renders html code""" 542 | LOGGER.debug("Test template tag not showing terms if not agreed to") 543 | rendered = self.render_template(self.template_string_1) 544 | terms = TermsAndConditions.get_active() 545 | self.assertIn(terms.slug, rendered) 546 | 547 | def test_not_show_terms_if_agreed(self): 548 | """test if show_terms_if_not_agreed template tag does not load if user agreed terms""" 549 | LOGGER.debug("Test template tag not showing terms once agreed to") 550 | terms = TermsAndConditions.get_active() 551 | UserTermsAndConditions.objects.create(terms=terms, user=self.user1) 552 | rendered = self.render_template(self.template_string_1) 553 | self.assertNotIn(terms.slug, rendered) 554 | 555 | def test_show_terms_if_not_agreed_on_protected_url_not_agreed(self): 556 | """Check terms on protected url if not agreed""" 557 | context = self._make_context("/test") 558 | result = show_terms_if_not_agreed(context) 559 | terms = TermsAndConditions.get_active(slug=DEFAULT_TERMS_SLUG) 560 | self.assertEqual(result.get("not_agreed_terms")[0], terms) 561 | 562 | def test_show_terms_if_not_agreed_on_unprotected_url_not_agreed(self): 563 | """Check terms on unprotected url if not agreed""" 564 | context = self._make_context("/") 565 | result = show_terms_if_not_agreed(context) 566 | self.assertDictEqual(result, {"not_agreed_terms": False}) 567 | 568 | def test_as_template(self): 569 | """Test as_template template tag""" 570 | terms = TermsAndConditions.get_active() 571 | rendered = Template(self.template_string_3).render(Context({"terms": terms})) 572 | self.assertIn(terms.text, rendered) 573 | -------------------------------------------------------------------------------- /termsandconditions/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Master URL Pattern List for the application. Most of the patterns here should be top-level 3 | pass-offs to sub-modules, who will have their own urls.py defining actions within. 4 | """ 5 | 6 | from django.contrib import admin 7 | from django.urls import path, register_converter 8 | from django.views.decorators.cache import never_cache 9 | 10 | from .views import AcceptTermsView, EmailTermsView, TermsActiveView, TermsView 11 | 12 | admin.autodiscover() 13 | 14 | 15 | class TermsVersionConverter: 16 | """ 17 | Registers Django URL path converter for Terms Version Numbers 18 | """ 19 | 20 | regex = "[0-9.]+" 21 | 22 | def to_python(self, value): 23 | return value 24 | 25 | def to_url(self, value): 26 | return value 27 | 28 | 29 | register_converter(TermsVersionConverter, "termsversion") 30 | 31 | urlpatterns = ( 32 | # View Unaccepted Terms 33 | path("", never_cache(TermsView.as_view()), name="tc_view_page"), 34 | # View Specific Active Terms 35 | path( 36 | "view//", 37 | never_cache(TermsView.as_view()), 38 | name="tc_view_specific_page", 39 | ), 40 | # View Specific Version of Terms 41 | path( 42 | "view///", 43 | never_cache(TermsView.as_view()), 44 | name="tc_view_specific_version_page", 45 | ), 46 | # View All Active Terms 47 | path("active/", never_cache(TermsActiveView.as_view()), name="tc_view_active_page"), 48 | # Print Specific Version of Terms 49 | path( 50 | "print///", 51 | never_cache( 52 | TermsView.as_view(template_name="termsandconditions/tc_print_terms.html") 53 | ), 54 | name="tc_print_page", 55 | ), 56 | # Accept Terms 57 | path("accept/", AcceptTermsView.as_view(), name="tc_accept_page"), 58 | # Accept Specific Terms 59 | path( 60 | "accept//", 61 | AcceptTermsView.as_view(), 62 | name="tc_accept_specific_page", 63 | ), 64 | # Accept Specific Terms Version 65 | path( 66 | "accept///", 67 | AcceptTermsView.as_view(), 68 | name="tc_accept_specific_version_page", 69 | ), 70 | # Email Terms 71 | path("email/", EmailTermsView.as_view(), name="tc_email_page"), 72 | # Email Specific Terms Version 73 | path( 74 | "email///", 75 | never_cache(EmailTermsView.as_view()), 76 | name="tc_specific_version_page", 77 | ), 78 | ) 79 | -------------------------------------------------------------------------------- /termsandconditions/views.py: -------------------------------------------------------------------------------- 1 | """Django Views for the termsandconditions module""" 2 | 3 | from django.contrib.auth.models import User 4 | from django.db import IntegrityError 5 | 6 | from .forms import UserTermsAndConditionsModelForm, EmailTermsForm 7 | from .models import TermsAndConditions, UserTermsAndConditions 8 | from django.conf import settings 9 | from django.core.mail import send_mail 10 | from django.contrib import messages 11 | from django.http import HttpResponseRedirect 12 | from django.utils.translation import gettext as _ 13 | from django.views.generic import DetailView, CreateView, FormView 14 | from django.template.loader import get_template 15 | from django.utils.encoding import iri_to_uri 16 | import logging 17 | from smtplib import SMTPException 18 | 19 | LOGGER = logging.getLogger(name="termsandconditions") 20 | DEFAULT_TERMS_BASE_TEMPLATE = "base.html" 21 | DEFAULT_TERMS_IP_HEADER_NAME = "REMOTE_ADDR" 22 | 23 | 24 | class GetTermsViewMixin: 25 | """Checks URL parameters for slug and/or version to pull the right TermsAndConditions object""" 26 | 27 | def get_terms(self, kwargs): 28 | """Checks URL parameters for slug and/or version to pull the right TermsAndConditions object""" 29 | 30 | slug = kwargs.get("slug") 31 | version = kwargs.get("version") 32 | 33 | if slug and version: 34 | terms = [ 35 | TermsAndConditions.objects.filter( 36 | slug=slug, version_number=version 37 | ).latest("date_active") 38 | ] 39 | elif slug: 40 | terms = [TermsAndConditions.get_active(slug)] 41 | else: 42 | # Return a list of not agreed to terms for the current user for the list view 43 | terms = TermsAndConditions.get_active_terms_not_agreed_to(self.request.user) 44 | return terms 45 | 46 | def get_return_to(self, from_dict): 47 | return_to = from_dict.get("returnTo", "/") 48 | 49 | if self.is_safe_url(return_to): 50 | # Django recommends to use this together with the helper above 51 | return iri_to_uri(return_to) 52 | 53 | LOGGER.debug(f"Unsafe URL found: {return_to}") 54 | return "/" 55 | 56 | def is_safe_url(self, url): 57 | # In Django 3.0 is_safe_url is renamed, so we import conditionally: 58 | # https://docs.djangoproject.com/en/3.2/releases/3.0/#id3 59 | try: 60 | from django.utils.http import url_has_allowed_host_and_scheme 61 | except ImportError: 62 | from django.utils.http import ( 63 | is_safe_url as url_has_allowed_host_and_scheme, 64 | ) 65 | 66 | return url_has_allowed_host_and_scheme(url, settings.ALLOWED_HOSTS) 67 | 68 | 69 | class AcceptTermsView(CreateView, GetTermsViewMixin): 70 | """ 71 | Terms and Conditions Acceptance view 72 | 73 | url: /terms/accept 74 | """ 75 | 76 | model = UserTermsAndConditions 77 | form_class = UserTermsAndConditionsModelForm 78 | template_name = "termsandconditions/tc_accept_terms.html" 79 | 80 | def get_context_data(self, **kwargs): 81 | """Pass additional context data""" 82 | context = super().get_context_data(**kwargs) 83 | context["terms_base_template"] = getattr( 84 | settings, "TERMS_BASE_TEMPLATE", DEFAULT_TERMS_BASE_TEMPLATE 85 | ) 86 | return context 87 | 88 | def get_initial(self): 89 | """Override of CreateView method, queries for which T&C to accept and catches returnTo from URL""" 90 | LOGGER.debug("termsandconditions.views.AcceptTermsView.get_initial") 91 | 92 | terms = self.get_terms(self.kwargs) 93 | return_to = self.get_return_to(self.request.GET) 94 | 95 | return {"terms": terms, "returnTo": return_to} 96 | 97 | def post(self, request, *args, **kwargs): 98 | """ 99 | Handles POST request. 100 | """ 101 | return_url = self.get_return_to(self.request.POST) 102 | terms_ids = request.POST.getlist("terms") 103 | 104 | if not terms_ids: # pragma: nocover 105 | return HttpResponseRedirect(return_url) 106 | 107 | if request.user.is_authenticated: 108 | user = request.user 109 | else: 110 | # Get user out of saved pipeline from django-socialauth 111 | if "partial_pipeline" in request.session: 112 | user_pk = request.session["partial_pipeline"]["kwargs"]["user"]["pk"] 113 | user = User.objects.get(id=user_pk) 114 | else: 115 | return HttpResponseRedirect("/") 116 | 117 | store_ip_address = getattr(settings, "TERMS_STORE_IP_ADDRESS", True) 118 | if store_ip_address: 119 | ip_address = request.META.get( 120 | getattr(settings, "TERMS_IP_HEADER_NAME", DEFAULT_TERMS_IP_HEADER_NAME) 121 | ) 122 | if "," in ip_address: 123 | ip_address = ip_address.split(",")[0].strip() 124 | else: 125 | ip_address = "" 126 | 127 | for terms_id in terms_ids: 128 | try: 129 | new_user_terms = UserTermsAndConditions( 130 | user=user, 131 | terms=TermsAndConditions.objects.get(pk=int(terms_id)), 132 | ip_address=ip_address, 133 | ) 134 | new_user_terms.save() 135 | except IntegrityError: # pragma: nocover 136 | pass 137 | 138 | return HttpResponseRedirect(return_url) 139 | 140 | 141 | class EmailTermsView(FormView, GetTermsViewMixin): 142 | """ 143 | Email Terms and Conditions View 144 | 145 | url: /terms/email 146 | """ 147 | 148 | template_name = "termsandconditions/tc_email_terms_form.html" 149 | 150 | form_class = EmailTermsForm 151 | 152 | def get_context_data(self, **kwargs): 153 | """Pass additional context data""" 154 | context = super().get_context_data(**kwargs) 155 | context["terms_base_template"] = getattr( 156 | settings, "TERMS_BASE_TEMPLATE", DEFAULT_TERMS_BASE_TEMPLATE 157 | ) 158 | return context 159 | 160 | def get_initial(self): 161 | """Override of CreateView method, queries for which T&C send, catches returnTo from URL""" 162 | LOGGER.debug("termsandconditions.views.EmailTermsView.get_initial") 163 | 164 | terms = self.get_terms(self.kwargs) 165 | 166 | return_to = self.get_return_to(self.request.GET) 167 | 168 | return {"terms": terms, "returnTo": return_to} 169 | 170 | def form_valid(self, form): 171 | """Override of CreateView method, sends the email.""" 172 | LOGGER.debug("termsandconditions.views.EmailTermsView.form_valid") 173 | 174 | template = get_template("termsandconditions/tc_email_terms.html") 175 | template_rendered = template.render({"terms": form.cleaned_data.get("terms")}) 176 | 177 | LOGGER.debug("Email Terms Body:") 178 | LOGGER.debug(template_rendered) 179 | 180 | try: 181 | send_mail( 182 | form.cleaned_data.get("email_subject", _("Terms")), 183 | template_rendered, 184 | settings.DEFAULT_FROM_EMAIL, 185 | [form.cleaned_data.get("email_address")], 186 | fail_silently=False, 187 | ) 188 | messages.add_message( 189 | self.request, messages.INFO, _("Terms and Conditions Sent.") 190 | ) 191 | except SMTPException: # pragma: no cover 192 | messages.add_message( 193 | self.request, 194 | messages.ERROR, 195 | _("An Error Occurred Sending Your Message."), 196 | ) 197 | 198 | self.success_url = self.get_return_to(form.cleaned_data) 199 | 200 | return super().form_valid(form) 201 | 202 | def form_invalid(self, form): 203 | """Override of CreateView method, logs invalid email form submissions.""" 204 | LOGGER.debug("Invalid Email Form Submitted") 205 | messages.add_message(self.request, messages.ERROR, _("Invalid Email Address.")) 206 | return super().form_invalid(form) 207 | 208 | 209 | class TermsView(DetailView, GetTermsViewMixin): 210 | """ 211 | View Unaccepted Terms and Conditions View 212 | 213 | url: /terms/ 214 | """ 215 | 216 | template_name = "termsandconditions/tc_view_terms.html" 217 | context_object_name = "terms_list" 218 | 219 | def get_context_data(self, **kwargs): 220 | """Pass additional context data""" 221 | context = super().get_context_data(**kwargs) 222 | context["terms_base_template"] = getattr( 223 | settings, "TERMS_BASE_TEMPLATE", DEFAULT_TERMS_BASE_TEMPLATE 224 | ) 225 | return context 226 | 227 | def get_object(self, queryset=None): 228 | """Override of DetailView method, queries for which T&C to return""" 229 | LOGGER.debug("termsandconditions.views.TermsView.get_object") 230 | return self.get_terms(self.kwargs) 231 | 232 | 233 | class TermsActiveView(TermsView): 234 | """ 235 | View Active Terms and Conditions View 236 | 237 | url: /terms/active/ 238 | """ 239 | 240 | def get_object(self, queryset=None): 241 | """Override of DetailView method, queries for which T&C to return""" 242 | LOGGER.debug("termsandconditions.views.AllTermsView.get_object") 243 | return TermsAndConditions.get_active_terms_list() 244 | -------------------------------------------------------------------------------- /termsandconditions_demo/__init__.py: -------------------------------------------------------------------------------- 1 | """Demo app for termsandconditions""" 2 | -------------------------------------------------------------------------------- /termsandconditions_demo/mediaroot/README.txt: -------------------------------------------------------------------------------- 1 | Placeholder for mediaroot dir. -------------------------------------------------------------------------------- /termsandconditions_demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django Settings File 3 | 4 | INSTRUCTIONS 5 | SAVE A COPY OF THIS FILE IN THIS DIRECTORY WITH THE NAME local_settings.py 6 | MAKE YOUR LOCAL SETTINGS CHANGES IN THAT FILE AND DO NOT CHECK IT IN 7 | CHANGES TO THIS FILE SHOULD BE TO ADD/REMOVE SETTINGS THAT NEED TO BE 8 | MADE LOCALLY BY ALL INSTALLATIONS 9 | 10 | local_settings.py, once created, should never be checked into source control 11 | It is ignored by default by .gitignore, so if you don't mess with that, you should be fine. 12 | """ 13 | import os 14 | import logging 15 | 16 | # Set the root path of the project so it's not hard coded 17 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) 18 | 19 | DEBUG = True 20 | # IP Addresses that should be treated as internal/debug users 21 | INTERNAL_IPS = ("127.0.0.1",) 22 | ALLOWED_HOSTS = ("localhost", "127.0.0.1") 23 | 24 | # Cache Settings 25 | CACHE_MIDDLEWARE_SECONDS = 30 26 | CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True 27 | CACHE_MIDDLEWARE_KEY_PREFIX = "tc" 28 | 29 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 30 | # trailing slash if there is a path component (optional in other cases). 31 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 32 | MEDIA_URL = "/media/" 33 | 34 | # Absolute path to the directory that holds media. 35 | # Note that as of Django 1.3 - media is for uploaded files only. 36 | # Example: "/home/media/media.lawrence.com/" 37 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, "mediaroot") 38 | 39 | # Staticfiles Config 40 | STATIC_ROOT = os.path.join(PROJECT_ROOT, "staticroot") 41 | STATIC_URL = "/static/" 42 | STATICFILES_DIRS = [os.path.join(PROJECT_ROOT, "static")] 43 | 44 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 45 | # trailing slash. 46 | # Examples: "http://foo.com/media/", "/media/". 47 | ADMIN_MEDIA_PREFIX = STATIC_URL + "admin/" 48 | 49 | # DB settings 50 | if os.environ.get("TERMS_DATABASE", "sqlite") == "postgresql": 51 | DATABASES = { 52 | "default": { 53 | "ENGINE": "django.db.backends.postgresql_psycopg2", 54 | "NAME": "termsandconditions", 55 | "USER": "termsandconditions", 56 | "PASSWORD": "", 57 | "HOST": "127.0.0.1", 58 | "PORT": "", # Set to empty string for default. 59 | "SUPPORTS_TRANSACTIONS": "true", 60 | }, 61 | } 62 | else: 63 | DATABASES = { 64 | "default": { 65 | "ENGINE": "django.db.backends.sqlite3", 66 | "NAME": os.path.join(PROJECT_ROOT, "termsandconditions.db"), 67 | "SUPPORTS_TRANSACTIONS": "false", 68 | } 69 | } 70 | 71 | # Python dotted path to the WSGI application used by Django's runserver. 72 | WSGI_APPLICATION = "termsandconditions_demo.wsgi.application" 73 | 74 | # Local time zone for this installation. Choices can be found here: 75 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 76 | # although not all choices may be available on all operating systems. 77 | # If running in a Windows environment this must be set to the same as your 78 | # system time zone. 79 | TIME_ZONE = "America/Denver" 80 | 81 | SITE_ID = 1 82 | 83 | # If you set this to False, Django will make some optimizations so as not 84 | # to load the internationalization machinery. 85 | USE_I18N = False 86 | 87 | # Make this unique, and don't share it with anybody. 88 | SECRET_KEY = "zv$+w7juz@(g!^53o0ai1uF82)=jkz9my_r=3)fglrj5t8l$2#" 89 | 90 | # Default Auto Field Class 91 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 92 | 93 | # Email Settings 94 | EMAIL_HOST = "a real smtp server" 95 | EMAIL_HOST_USER = "your_mailbox_username" 96 | EMAIL_HOST_PASSWORD = "your_mailbox_password" 97 | DEFAULT_FROM_EMAIL = "a real email address" 98 | SERVER_EMAIL = "a real email address" 99 | 100 | """ Django settings for the project. """ 101 | 102 | # List of callables that know how to import templates from various sources. 103 | TEMPLATE_LOADERS_LIST = ( 104 | "django.template.loaders.filesystem.Loader", 105 | "django.template.loaders.app_directories.Loader", 106 | ) 107 | 108 | # List of callables that add their data to each template 109 | 110 | # List of directories to find templates in 111 | TEMPLATE_DIRS_LIST = [os.path.join(PROJECT_ROOT, "templates")] 112 | 113 | # Whether Template Debug is enabled 114 | TEMPLATE_DEBUG_SETTING = DEBUG 115 | 116 | # Needed to configure Django 1.8+ 117 | TEMPLATES = [ 118 | { 119 | "BACKEND": "django.template.backends.django.DjangoTemplates", 120 | "DIRS": TEMPLATE_DIRS_LIST, 121 | "OPTIONS": { 122 | "context_processors": ( 123 | "django.contrib.auth.context_processors.auth", 124 | "django.template.context_processors.debug", 125 | "django.template.context_processors.media", 126 | "django.template.context_processors.static", 127 | "django.template.context_processors.request", 128 | "django.contrib.messages.context_processors.messages", 129 | ), 130 | "debug": TEMPLATE_DEBUG_SETTING, 131 | "loaders": TEMPLATE_LOADERS_LIST, 132 | }, 133 | }, 134 | ] 135 | 136 | # For use Django 1.10+ 137 | MIDDLEWARE = ( 138 | "django.middleware.cache.UpdateCacheMiddleware", 139 | "django.middleware.gzip.GZipMiddleware", 140 | "django.middleware.http.ConditionalGetMiddleware", 141 | "django.contrib.sessions.middleware.SessionMiddleware", 142 | "django.contrib.auth.middleware.AuthenticationMiddleware", 143 | "termsandconditions.middleware.TermsAndConditionsRedirectMiddleware", 144 | "django.middleware.common.CommonMiddleware", 145 | "django.contrib.messages.middleware.MessageMiddleware", 146 | "django.middleware.cache.FetchFromCacheMiddleware", 147 | ) 148 | 149 | ROOT_URLCONF = "termsandconditions_demo.urls" 150 | 151 | INSTALLED_APPS = ( 152 | "django.contrib.admin", 153 | "django.contrib.admindocs", 154 | "django.contrib.auth", 155 | "django.contrib.contenttypes", 156 | "django.contrib.messages", 157 | "django.contrib.sessions", 158 | "django.contrib.sites", 159 | "django.contrib.staticfiles", 160 | "termsandconditions", 161 | ) 162 | 163 | # Custom Variables Below Here ####### 164 | 165 | LOGIN_REDIRECT_URL = "/" 166 | 167 | # Terms & Conditions (termsandconditions) Settings ####### 168 | DEFAULT_TERMS_SLUG = "site-terms" 169 | ACCEPT_TERMS_PATH = "/terms/accept/" 170 | TERMS_EXCLUDE_URL_PREFIX_LIST = {"/admin", "/terms"} 171 | TERMS_EXCLUDE_URL_LIST = {"/", "/termsrequired/", "/accounts/logout/", "/securetoo/"} 172 | TERMS_EXCLUDE_URL_CONTAINS_LIST = ( 173 | {} 174 | ) # Useful if you are using internationalization and your URLs could change per language 175 | TERMS_CACHE_SECONDS = 30 176 | TERMS_EXCLUDE_USERS_WITH_PERM = "auth.can_skip_t&c" 177 | TERMS_IP_HEADER_NAME = "REMOTE_ADDR" 178 | TERMS_STORE_IP_ADDRESS = True 179 | 180 | # LOGGING ###### 181 | # Catch Python warnings (e.g. deprecation warnings) into the logger 182 | try: 183 | logging.captureWarnings(True) 184 | except AttributeError: # python 2.6 does not do this # pragma: nocover 185 | pass 186 | 187 | LOGGING = { 188 | "version": 1, 189 | "disable_existing_loggers": False, 190 | "formatters": { 191 | "verbose": { 192 | "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s" 193 | }, 194 | "simple": {"format": "%(levelname)s %(message)s"}, 195 | }, 196 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 197 | "handlers": { 198 | "mail_admins": { 199 | "level": "ERROR", 200 | "filters": ["require_debug_false"], 201 | "class": "django.utils.log.AdminEmailHandler", 202 | }, 203 | "console": { 204 | "level": "DEBUG", 205 | "class": "logging.StreamHandler", 206 | "formatter": "simple", 207 | }, 208 | }, 209 | "loggers": { 210 | "py.warnings": { 211 | "handlers": ["console"], 212 | "level": "WARNING", 213 | "propagate": True, 214 | }, 215 | "django.db.backends": { 216 | "handlers": ["console"], 217 | "level": "DEBUG", 218 | }, 219 | "django.request": { 220 | "handlers": ["mail_admins", "console"], 221 | "level": "ERROR", 222 | "propagate": True, 223 | }, 224 | "termsandconditions": { 225 | "handlers": ["console"], 226 | "level": "DEBUG", 227 | "propagate": True, 228 | }, 229 | }, 230 | } 231 | -------------------------------------------------------------------------------- /termsandconditions_demo/settings_local_template.py: -------------------------------------------------------------------------------- 1 | """Local Django Settings, Copy This File As settings_local.py and Make Local Changes""" 2 | 3 | from .settings import * 4 | 5 | # Local Overrides Here 6 | # Local DB settings. (Postgres) 7 | DATABASES = { 8 | # 'default': { 9 | # 'ENGINE': 'django.db.backends.postgresql_psycopg2', 10 | # 'NAME': 'termsandconditions', 11 | # 'USER': 'termsandconditions', 12 | # 'PASSWORD': '', 13 | # 'HOST': '127.0.0.1', 14 | # 'PORT': '', # Set to empty string for default. 15 | # 'SUPPORTS_TRANSACTIONS': 'true', 16 | # }, 17 | "default": { 18 | "ENGINE": "django.db.backends.sqlite3", 19 | "NAME": PROJECT_ROOT + "/termsandconditions.db", 20 | "SUPPORTS_TRANSACTIONS": "false", 21 | } 22 | } 23 | 24 | # Cache Settings 25 | CACHES = { 26 | "default": { 27 | "BACKEND": "dummy:///", 28 | "LOCATION": "", 29 | "OPTIONS": {"PASSWORD": ""}, 30 | }, 31 | } 32 | CACHE_MIDDLEWARE_SECONDS = 30 33 | CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True 34 | CACHE_MIDDLEWARE_KEY_PREFIX = "tc" 35 | 36 | # Make this unique, and don't share it with anybody. 37 | SECRET_KEY = "12345" 38 | 39 | # Email Settings 40 | EMAIL_HOST = "a real smtp server" 41 | EMAIL_HOST_USER = "your_mailbox_username" 42 | EMAIL_HOST_PASSWORD = "your_mailbox_password" 43 | DEFAULT_FROM_EMAIL = "a real email address" 44 | SERVER_EMAIL = "a real email address" 45 | -------------------------------------------------------------------------------- /termsandconditions_demo/static/css/termsandconditions.css: -------------------------------------------------------------------------------- 1 | #tc-terms-html {width:80%;margin: auto;border: 1px solid #666;background-color: #ccc;padding: 8px;} 2 | #tc-terms-form {text-align: center;} 3 | .errorlist li {font-weight: bold;margin: 15px 15px 15px 15px;color:#ff0000;} -------------------------------------------------------------------------------- /termsandconditions_demo/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyface/django-termsandconditions/8a2902539f51985ff81574f1f86cff8e464d6b12/termsandconditions_demo/static/images/favicon.ico -------------------------------------------------------------------------------- /termsandconditions_demo/staticroot/README.txt: -------------------------------------------------------------------------------- 1 | Placeholder for static dir. -------------------------------------------------------------------------------- /termsandconditions_demo/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}File Not Found{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

 I'm sorry, we don't have a page with that URL.

9 | {% if messages %} 10 | {% for msg in messages %} 11 |

{{ msg.message }} ({{ msg.extra_tags }})

12 | {% endfor %} 13 | {% endif %} 14 | {% if error_msg %} 15 |

Details:

16 |

{{ error_msg }}

17 | {% endif %} 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /termsandconditions_demo/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}Server Error{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

 I'm sorry, an error has occurred with the 9 | application.

10 | {% if messages %} 11 | {% for msg in messages %} 12 |

{{ msg.message }} ({{ msg.extra_tags }})

13 | {% endfor %} 14 | {% endif %} 15 | {% if error_msg %} 16 |

Details:

17 |

{{ error_msg }}

18 | {% endif %} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /termsandconditions_demo/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block headtitle %}Django Terms and Conditions{% endblock %} 15 | 16 | {% block metatags %} 17 | {% endblock %} 18 | {% if debug %} 19 | 22 | {% else %} 23 | 26 | {% endif %} 27 | 28 | 33 | {% block head %}{% endblock %} 34 | 35 | 36 |
37 | 38 | {% block header %}{% include "header.html" %}{% endblock %} 39 | 40 | {% block messages %} 41 | {% if messages %} 42 |
43 | {% for message in messages %} 44 |

{{ message }}

45 | {% endfor %} 46 |
47 | {% endif %} 48 | {% endblock %} 49 | 50 | {% block content %}

No Content Defined

{% endblock %} 51 | 52 | {% block footer %}{% include "footer.html" %}{% endblock %} 53 | 54 | {% block bottom_js %}{% endblock %} 55 |
56 | 57 | -------------------------------------------------------------------------------- /termsandconditions_demo/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}Login Error{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

 I'm sorry, an error prevented you from 9 | logging in, please try again.

10 | {% if messages %} 11 | {% for msg in messages %} 12 |

{{ msg.message }} ({{ msg.extra_tags }})

13 | {% endfor %} 14 | {% endif %} 15 | {% if error_msg %} 16 |

Details:

17 |

{{ error_msg }}

18 | {% endif %} 19 |
20 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions_demo/templates/footer.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /termsandconditions_demo/templates/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
    4 | {% if user.is_authenticated %} 5 |
  • Home
  • 6 |
  • Sign Out
  • 7 | {% else %} 8 |
  • Home
  • 9 |
  • Sign In
  • 10 | {% endif %} 11 |
12 |
13 |
-------------------------------------------------------------------------------- /termsandconditions_demo/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}Django Terms and Conditions Demo - Home{% endblock %} 4 | 5 | {% block body_class %}{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | {% block header %}{% endblock %} 10 | 11 | {% block content %} 12 |
13 | {% if not user.is_authenticated %} 14 |

Click the button below to get started.

15 | 16 |

You'll be asked to accept the site terms and conditions.

17 | 18 | Get Started 19 | {% else %} 20 |

Thank you for signing in, {{ user.username }}.

21 | Return to Secure Area 22 | {% endif %} 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /termsandconditions_demo/templates/index_anon.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}Django Terms and Conditions Demo - Anon Home{% endblock %} 4 | 5 | {% block body_class %}{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | {% block header %}{% endblock %} 10 | 11 | {% block content %} 12 |
13 | Anon Redirect Page. 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /termsandconditions_demo/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

You have been logged out.

6 |
7 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions_demo/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 | 10 | 11 |
12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions_demo/templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /termsandconditions_demo/templates/secure.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}Secure Area{% endblock %} 4 | 5 | {% block body_class %}{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |

Welcome to the secure area, {{ user.username }}.

12 | 13 |

This is the area of the web site where the exciting stuff happens.

14 | 15 |

There is another secure page you can visit as well.

16 |
17 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions_demo/templates/securetoo.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block headtitle %}Secure Area{% endblock %} 4 | 5 | {% block body_class %}{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |

Welcome to the SECOND secure area.

12 | 13 |

Return to the main secure page.

14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions_demo/templates/terms_required.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Terms and Conditions Acceptance Required

6 | 7 |

8 | You should have had to accept the terms and conditions before seeing this page. 9 |

10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /termsandconditions_demo/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Master path Pattern List for the application. Most of the patterns here should be top-level 3 | pass-offs to sub-modules, who will have their own paths.py defining actions within. 4 | """ 5 | 6 | from django.contrib import admin 7 | from django.conf import settings 8 | from django.urls import path, include 9 | 10 | from .views import TermsRequiredView, SecureView, IndexView 11 | from django.views.generic import RedirectView, TemplateView 12 | from django.views.decorators.cache import never_cache 13 | from termsandconditions.decorators import terms_required 14 | from django.contrib.auth.decorators import login_required 15 | 16 | admin.autodiscover() 17 | 18 | urlpatterns = ( 19 | # Home Page 20 | path("", never_cache(IndexView.as_view()), name="tc_demo_home_page"), 21 | # Home Page 22 | path( 23 | "anon/", 24 | never_cache(IndexView.as_view(template_name="index_anon.html")), 25 | name="tc_demo_home_anon_page", 26 | ), # used for pipeline user test 27 | # Secure Page 28 | path( 29 | "secure/", 30 | never_cache(login_required(SecureView.as_view())), 31 | name="tc_demo_secure_page", 32 | ), 33 | # Secure Page Too 34 | path( 35 | "securetoo/", 36 | never_cache(login_required(SecureView.as_view(template_name="securetoo.html"))), 37 | name="tc_demo_secure_page_too", 38 | ), 39 | # Terms Required 40 | path( 41 | "termsrequired/", 42 | never_cache(terms_required(login_required(TermsRequiredView.as_view()))), 43 | name="tc_demo_required_page", 44 | ), 45 | # Terms and Conditions 46 | path("terms/", include("termsandconditions.urls")), 47 | # Auth paths: 48 | path("accounts/", include("django.contrib.auth.urls")), 49 | # Admin documentation: 50 | path("admin/doc/", include("django.contrib.admindocs.urls")), 51 | # Admin Site: 52 | path("admin/", admin.site.urls), 53 | # Robots and Favicon 54 | path( 55 | "robots.txt", 56 | TemplateView.as_view(), 57 | {"template": "robots.txt", "mimetype": "text/plain"}, 58 | ), 59 | path( 60 | "favicon.ico", 61 | RedirectView.as_view(permanent=True), 62 | {"path": settings.STATIC_URL + "images/favicon.ico"}, 63 | ), 64 | ) 65 | -------------------------------------------------------------------------------- /termsandconditions_demo/views.py: -------------------------------------------------------------------------------- 1 | """Django Views for the termsandconditions-demo module""" 2 | 3 | from django.views.generic import TemplateView 4 | 5 | 6 | class IndexView(TemplateView): 7 | """ 8 | Main site page page. 9 | 10 | url: / 11 | """ 12 | 13 | template_name = "index.html" 14 | 15 | 16 | class SecureView(TemplateView): 17 | """ 18 | Secure testing page. 19 | 20 | url: /secure & /securetoo 21 | """ 22 | 23 | template_name = "secure.html" 24 | 25 | 26 | class TermsRequiredView(TemplateView): 27 | """ 28 | Terms Required testing page. 29 | 30 | url: /terms_required 31 | """ 32 | 33 | template_name = "terms_required.html" 34 | -------------------------------------------------------------------------------- /termsandconditions_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI File that enables Apache/GUnicorn to run Django""" 2 | 3 | import os 4 | import sys 5 | from django.core.wsgi import get_wsgi_application 6 | 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.abspath(os.pardir), os.pardir))) 8 | sys.path.insert( 9 | 0, os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)))) 10 | ) 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "termsandconditions_demo.settings") 13 | 14 | # This application object is used by any WSGI server configured to use this 15 | # file. This includes Django's development server, if the WSGI_APPLICATION 16 | # setting points here. 17 | 18 | application = get_wsgi_application() 19 | --------------------------------------------------------------------------------