├── .editorconfig ├── .gitattributes ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .travis.yml ├── CONTRIBUTORS.txt ├── GOALS ├── LICENSE ├── Makefile ├── README.rst ├── api ├── __init__.py ├── admin.py ├── apps.py ├── constants.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200610_1213.py │ ├── 0003_auto_20200610_1313.py │ ├── 0004_auto_20200610_1316.py │ ├── 0005_auto_20200610_1648.py │ ├── 0006_auto_20200610_1736.py │ ├── 0007_game_uuid.py │ ├── 0008_auto_20200610_1904.py │ ├── 0009_auto_20200611_0905.py │ ├── 0010_auto_20200611_1027.py │ ├── 0011_auto_20200611_1029.py │ ├── 0012_auto_20200611_1101.py │ ├── 0013_board_game_uuid.py │ ├── 0014_auto_20200611_1750.py │ ├── 0015_remove_player_guest.py │ ├── 0016_auto_20200614_1012.py │ ├── 0017_auto_20200614_1133.py │ ├── 0018_auto_20200615_1345.py │ ├── 0019_auto_20200615_1843.py │ ├── 0020_auto_20200615_1851.py │ ├── 0021_piece_square.py │ ├── 0022_auto_20200618_1134.py │ ├── 0023_auto_20200618_1152.py │ ├── 0024_board_castling_rights.py │ ├── 0025_auto_20200618_1319.py │ ├── 0026_auto_20200618_1400.py │ ├── 0027_auto_20200618_1422.py │ ├── 0028_auto_20200618_1422.py │ ├── 0029_auto_20200618_1426.py │ ├── 0030_auto_20200618_1434.py │ ├── 0031_auto_20200618_1636.py │ ├── 0032_board_board_fen.py │ ├── 0033_auto_20200625_1202.py │ ├── 0034_board_board_fen_flipped.py │ ├── 0035_auto_20200715_1212.py │ ├── 0036_auto_20200716_1021.py │ ├── 0037_auto_20200716_1115.py │ ├── 0038_remove_elo_k_factor.py │ ├── 0039_elo_previous_rating.py │ ├── 0040_elo_uuid.py │ ├── 0041_auto_20200721_1015.py │ └── __init__.py ├── models.py ├── permissions.py ├── pgn_games │ └── fools_mate.pgn ├── serializers.py ├── services.py ├── tests │ ├── fixtures.py │ ├── test_api.py │ ├── test_models.py │ ├── test_services.py │ └── test_token.py └── views.py ├── chess_api_project ├── __init__.py ├── conftest.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ └── __init__.py ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ ├── js │ │ └── project.js │ └── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss ├── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── account │ │ ├── account_inactive.html │ │ ├── base.html │ │ ├── email.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── base.html │ ├── pages │ │ ├── about.html │ │ └── home.html │ └── users │ │ ├── user_detail.html │ │ └── user_form.html ├── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── api │ │ ├── serializers.py │ │ └── views.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_user_active.py │ │ └── __init__.py │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── test_drf_urls.py │ │ ├── test_drf_views.py │ │ ├── test_forms.py │ │ ├── test_models.py │ │ ├── test_urls.py │ │ └── test_views.py │ ├── urls.py │ └── views.py └── utils │ ├── __init__.py │ └── context_processors.py ├── config ├── __init__.py ├── api_router.py ├── routing.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── production.py │ └── test.py ├── urls.py └── wsgi.py ├── docs ├── #results.org# ├── Chess.postman_collection.json ├── Makefile ├── __init__.py ├── api_coverage │ ├── api___init___py.html │ ├── api_admin_py.html │ ├── api_apps_py.html │ ├── api_constants_py.html │ ├── api_models_py.html │ ├── api_permissions_py.html │ ├── api_serializers_py.html │ ├── api_services_py.html │ ├── api_urls_py.html │ ├── api_views_py.html │ ├── coverage_html.js │ ├── index.html │ ├── jquery.ba-throttle-debounce.min.js │ ├── jquery.hotkeys.js │ ├── jquery.isonscreen.js │ ├── jquery.min.js │ ├── jquery.tablesorter.min.js │ ├── keybd_closed.png │ ├── keybd_open.png │ ├── status.json │ ├── stream_app___init___py.html │ ├── stream_app_admin_py.html │ ├── stream_app_apps_py.html │ ├── stream_app_consumers_py.html │ ├── stream_app_models_py.html │ ├── stream_app_routing_py.html │ ├── stream_app_services_py.html │ ├── stream_app_views_py.html │ └── style.css ├── chess_api.pdf ├── client_implementation_guide.html ├── client_implementation_guide.org ├── conf.py ├── index.rst ├── make.bat ├── pgn │ └── Morphy.pgn ├── results.html ├── results.org └── time_control.org ├── locale └── README.rst ├── manage.py ├── openapi-schema.yml ├── pytest.ini ├── requirements ├── #local.txt# ├── base.txt ├── local.txt └── production.txt ├── setup.cfg ├── stream_app ├── __init__.py ├── admin.py ├── apps.py ├── consumers.py ├── migrations │ └── __init__.py ├── models.py ├── routing.py ├── services.py ├── tests │ ├── fixtures.py │ └── test_consumers.py └── views.py └── utility ├── install_os_dependencies.sh ├── install_python_dependencies.sh ├── requirements-bionic.apt ├── requirements-buster.apt ├── requirements-jessie.apt ├── requirements-stretch.apt ├── requirements-trusty.apt └── requirements-xenial.apt /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.py] 16 | line_length = 88 17 | known_first_party = chess_api_project,config 18 | multi_line_output = 3 19 | default_section = THIRDPARTY 20 | recursive = true 21 | skip = venv/ 22 | skip_glob = **/migrations/*.py 23 | include_trailing_comma = true 24 | force_grid_wrap = 0 25 | use_parentheses = true 26 | 27 | [*.{html,css,scss,json,yml}] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | [*.md] 32 | trim_trailing_whitespace = false 33 | 34 | [Makefile] 35 | indent_style = tab 36 | 37 | [nginx.conf] 38 | indent_style = space 39 | indent_size = 2 40 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | staticfiles/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv 63 | .python-version 64 | 65 | 66 | 67 | # Environments 68 | .venv 69 | venv/ 70 | ENV/ 71 | 72 | # Rope project settings 73 | .ropeproject 74 | 75 | # mkdocs documentation 76 | /site 77 | 78 | # mypy 79 | .mypy_cache/ 80 | 81 | 82 | ### Node template 83 | # Logs 84 | logs 85 | *.log 86 | npm-debug.log* 87 | yarn-debug.log* 88 | yarn-error.log* 89 | 90 | # Runtime data 91 | pids 92 | *.pid 93 | *.seed 94 | *.pid.lock 95 | 96 | # Directory for instrumented libs generated by jscoverage/JSCover 97 | lib-cov 98 | 99 | # Coverage directory used by tools like istanbul 100 | coverage 101 | 102 | # nyc test coverage 103 | .nyc_output 104 | 105 | # Bower dependency directory (https://bower.io/) 106 | bower_components 107 | 108 | # node-waf configuration 109 | .lock-wscript 110 | 111 | # Compiled binary addons (http://nodejs.org/api/addons.html) 112 | build/Release 113 | 114 | # Dependency directories 115 | node_modules/ 116 | jspm_packages/ 117 | 118 | # Typescript v1 declaration files 119 | typings/ 120 | 121 | # Optional npm cache directory 122 | .npm 123 | 124 | # Optional eslint cache 125 | .eslintcache 126 | 127 | # Optional REPL history 128 | .node_repl_history 129 | 130 | # Output of 'npm pack' 131 | *.tgz 132 | 133 | # Yarn Integrity file 134 | .yarn-integrity 135 | 136 | 137 | ### Linux template 138 | *~ 139 | 140 | # temporary files which can be created if a process still has a handle open of a deleted file 141 | .fuse_hidden* 142 | 143 | # KDE directory preferences 144 | .directory 145 | 146 | # Linux trash folder which might appear on any partition or disk 147 | .Trash-* 148 | 149 | # .nfs files are created when an open file is removed but is still being accessed 150 | .nfs* 151 | 152 | 153 | ### VisualStudioCode template 154 | .vscode/* 155 | !.vscode/settings.json 156 | !.vscode/tasks.json 157 | !.vscode/launch.json 158 | !.vscode/extensions.json 159 | 160 | 161 | 162 | 163 | 164 | ### Windows template 165 | # Windows thumbnail cache files 166 | Thumbs.db 167 | ehthumbs.db 168 | ehthumbs_vista.db 169 | 170 | # Dump file 171 | *.stackdump 172 | 173 | # Folder config file 174 | Desktop.ini 175 | 176 | # Recycle Bin used on file shares 177 | $RECYCLE.BIN/ 178 | 179 | # Windows Installer files 180 | *.cab 181 | *.msi 182 | *.msm 183 | *.msp 184 | 185 | # Windows shortcuts 186 | *.lnk 187 | 188 | 189 | ### macOS template 190 | # General 191 | *.DS_Store 192 | .AppleDouble 193 | .LSOverride 194 | 195 | # Icon must end with two \r 196 | Icon 197 | 198 | # Thumbnails 199 | ._* 200 | 201 | # Files that might appear in the root of a volume 202 | .DocumentRevisions-V100 203 | .fseventsd 204 | .Spotlight-V100 205 | .TemporaryItems 206 | .Trashes 207 | .VolumeIcon.icns 208 | .com.apple.timemachine.donotpresent 209 | 210 | # Directories potentially created on remote AFP share 211 | .AppleDB 212 | .AppleDesktop 213 | Network Trash Folder 214 | Temporary Items 215 | .apdisk 216 | 217 | 218 | ### SublimeText template 219 | # Cache files for Sublime Text 220 | *.tmlanguage.cache 221 | *.tmPreferences.cache 222 | *.stTheme.cache 223 | 224 | # Workspace files are user-specific 225 | *.sublime-workspace 226 | 227 | # Project files should be checked into the repository, unless a significant 228 | # proportion of contributors will probably not be using Sublime Text 229 | # *.sublime-project 230 | 231 | # SFTP configuration file 232 | sftp-config.json 233 | 234 | # Package control specific files 235 | Package Control.last-run 236 | Package Control.ca-list 237 | Package Control.ca-bundle 238 | Package Control.system-ca-bundle 239 | Package Control.cache/ 240 | Package Control.ca-certs/ 241 | Package Control.merged-ca-bundle 242 | Package Control.user-ca-bundle 243 | oscrypto-ca-bundle.crt 244 | bh_unicode_properties.cache 245 | 246 | # Sublime-github package stores a github token in this file 247 | # https://packagecontrol.io/packages/sublime-github 248 | GitHub.sublime-settings 249 | 250 | 251 | ### Vim template 252 | # Swap 253 | [._]*.s[a-v][a-z] 254 | [._]*.sw[a-p] 255 | [._]s[a-v][a-z] 256 | [._]sw[a-p] 257 | 258 | # Session 259 | Session.vim 260 | 261 | # Temporary 262 | .netrwhist 263 | 264 | # Auto-generated tag files 265 | tags 266 | 267 | 268 | ### VirtualEnv template 269 | # Virtualenv 270 | [Bb]in 271 | [Ii]nclude 272 | [Ll]ib 273 | [Ll]ib64 274 | [Ss]cripts 275 | pyvenv.cfg 276 | pip-selfcheck.json 277 | .env 278 | 279 | 280 | ### Project template 281 | 282 | chess_api_project/media/ 283 | 284 | .pytest_cache/ 285 | 286 | 287 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs|node_modules|migrations|.git|.tox' 2 | default_stages: [commit] 3 | fail_fast: true 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: master 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | 13 | - repo: https://github.com/psf/black 14 | rev: 19.10b0 15 | hooks: 16 | - id: black 17 | 18 | - repo: https://gitlab.com/pycqa/flake8 19 | rev: 3.8.1 20 | hooks: 21 | - id: flake8 22 | args: ['--config=setup.cfg'] 23 | additional_dependencies: [flake8-isort] 24 | 25 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | load-plugins=pylint_django 3 | 4 | [FORMAT] 5 | max-line-length=120 6 | 7 | [MESSAGES CONTROL] 8 | disable=missing-docstring,invalid-name 9 | 10 | [DESIGN] 11 | max-parents=13 12 | 13 | [TYPECHECK] 14 | generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: 5 | - pip install -r requirements/base.txt 6 | - pip install -r requirements/local.txt 7 | services: 8 | - postgresql 9 | - redis-server 10 | before_script: 11 | psql -c "create database chess_api_project;" -U postgres 12 | script: 13 | - make test 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Mikel Losada 2 | -------------------------------------------------------------------------------- /GOALS: -------------------------------------------------------------------------------- 1 | v0.1 2 | - Players can join games and make legal moves 3 | v0.2 4 | - Checks 5 | - Attacks 6 | - Game end detection 7 | v0.9 8 | - Repetition detection with half-move clock 9 | - Claims 10 | - Results 11 | - PGN parsing and writing 12 | - ELO 13 | - Code refactor 14 | - API Documentation and Postman collection 15 | v1.0 16 | - Player vs Stockfish mode 17 | 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tidyup: 2 | autoflake --recursive . --remove-all-unused-imports --remove-unused-variables --in-place 3 | isort **/*.py 4 | black . 5 | find . -name "*~" -type f -delete 6 | 7 | test: 8 | pytest api/tests stream_app/tests --cov=api --cov stream_app 9 | 10 | testpudb: 11 | pytest api/tests/ --pudb 12 | 13 | testreport: 14 | pytest --cov-report html:docs/api_coverage --cov=api --cov stream_app api/tests stream_app/tests 15 | 16 | coveralls: 17 | coverage run --source api,stream_app -m pytest api/tests stream_app/tests 18 | coveralls 19 | 20 | erdiagram: 21 | eralchemy -i postgres:///chess_api_project -o docs/chess_api.pdf 22 | 23 | openemacs: 24 | emacs api api/views.py & 25 | 26 | makemigrations: 27 | ./manage.py makemigrations 28 | 29 | migrate: 30 | ./manage.py migrate 31 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Capablanca Chess API 2 | ==================== 3 | 4 | Django Chess API powered by python-chess 5 | 6 | .. image:: https://api.travis-ci.com/WorkShoft/capablanca-api.svg?branch=master&status=passed 7 | :target: https://travis-ci.com/github/WorkShoft/capablanca-api 8 | :alt: TravisCI build status 9 | .. image:: https://coveralls.io/repos/github/WorkShoft/capablanca-api/badge.svg?branch=master 10 | :target: https://coveralls.io/github/WorkShoft/capablanca-api?branch=master 11 | :alt: Coverage status 12 | .. image:: https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg 13 | :target: https://github.com/pydanny/cookiecutter-django/ 14 | :alt: Built with Cookiecutter Django 15 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 16 | :target: https://github.com/ambv/black 17 | :alt: Black code style 18 | 19 | 20 | :License: Apache Software License 2.0 21 | 22 | 23 | Settings 24 | -------- 25 | 26 | Moved to settings_. 27 | 28 | .. _settings: http://cookiecutter-django.readthedocs.io/en/latest/settings.html 29 | 30 | Basic Commands 31 | -------------- 32 | 33 | Setting Up Your Users 34 | ^^^^^^^^^^^^^^^^^^^^^ 35 | 36 | * To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go. 37 | 38 | * To create an **superuser account**, use this command:: 39 | 40 | $ python manage.py createsuperuser 41 | 42 | For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users. 43 | 44 | Type checks 45 | ^^^^^^^^^^^ 46 | 47 | Running type checks with mypy: 48 | 49 | :: 50 | 51 | $ mypy chess_api_project 52 | 53 | Test coverage 54 | ^^^^^^^^^^^^^ 55 | 56 | To run the tests, check your test coverage, and generate an HTML coverage report:: 57 | 58 | $ coverage run -m pytest 59 | $ coverage html 60 | $ open htmlcov/index.html 61 | 62 | Running tests with py.test 63 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 64 | 65 | :: 66 | 67 | $ pytest 68 | 69 | Live reloading and Sass CSS compilation 70 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 71 | 72 | Moved to `Live reloading and SASS compilation`_. 73 | 74 | .. _`Live reloading and SASS compilation`: http://cookiecutter-django.readthedocs.io/en/latest/live-reloading-and-sass-compilation.html 75 | 76 | 77 | 78 | 79 | 80 | Deployment 81 | ---------- 82 | 83 | The following details how to deploy this application. 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/api/__init__.py -------------------------------------------------------------------------------- /api/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = "api" 6 | -------------------------------------------------------------------------------- /api/constants.py: -------------------------------------------------------------------------------- 1 | K_FACTOR = 32 2 | -------------------------------------------------------------------------------- /api/migrations/0002_auto_20200610_1213.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 10:13 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("api", "0001_initial"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="player", 18 | name="user", 19 | field=models.ForeignKey( 20 | null=True, 21 | on_delete=django.db.models.deletion.CASCADE, 22 | to=settings.AUTH_USER_MODEL, 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /api/migrations/0003_auto_20200610_1313.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 11:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0002_auto_20200610_1213"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="game", 15 | name="end_timestamp", 16 | field=models.DateTimeField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /api/migrations/0004_auto_20200610_1316.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 11:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0003_auto_20200610_1313"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", name="layout", field=models.TextField(), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /api/migrations/0005_auto_20200610_1648.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 14:48 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("api", "0004_auto_20200610_1316"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="player", 16 | name="elo", 17 | field=models.ForeignKey( 18 | null=True, on_delete=django.db.models.deletion.CASCADE, to="api.Elo" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /api/migrations/0006_auto_20200610_1736.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 15:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0005_auto_20200610_1648"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="layout", 16 | field=models.TextField( 17 | default="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="result", 22 | name="description", 23 | field=models.TextField( 24 | choices=[ 25 | ("Scheduled", "Scheduled"), 26 | ("Postponed", "Postponed"), 27 | ("Finished (no moves)", "Finished without any moves played"), 28 | ("In progress", "In progress"), 29 | ("Adjourned", "Adjourned"), 30 | ( 31 | "Finished (basic rules)", 32 | "Finished according to the Basic Rules of Play", 33 | ), 34 | ("Finished (clock)", "Finished by the clock"), 35 | ("Draw", "Draw"), 36 | ( 37 | "Finished (breach)", 38 | "Finished because of a breach of rules of one player", 39 | ), 40 | ( 41 | "Finished (compliance)", 42 | "Finished because both players persistently refuse to comply with the laws of chess", 43 | ), 44 | ("TBD", "To be decided"), 45 | ("Abandoned", "Abandoned"), 46 | ("Unknown", "Unknown"), 47 | ], 48 | default="In progress", 49 | ), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /api/migrations/0007_game_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 16:25 2 | 3 | import uuid 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("api", "0006_auto_20200610_1736"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="game", name="uuid", field=models.UUIDField(default=uuid.uuid4), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /api/migrations/0008_auto_20200610_1904.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 17:04 2 | 3 | import uuid 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("api", "0007_game_uuid"), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField(model_name="game", name="id",), 16 | migrations.AlterField( 17 | model_name="game", 18 | name="uuid", 19 | field=models.UUIDField( 20 | default=uuid.uuid4, primary_key=True, serialize=False 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /api/migrations/0009_auto_20200611_0905.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 07:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0008_auto_20200610_1904"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="elo", name="k_factor", field=models.IntegerField(default=32), 15 | ), 16 | migrations.AlterField( 17 | model_name="elo", name="rating", field=models.IntegerField(default=1200), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /api/migrations/0010_auto_20200611_1027.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 08:27 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("api", "0009_auto_20200611_0905"), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField(model_name="player", name="elo",), 17 | migrations.AddField( 18 | model_name="elo", 19 | name="player", 20 | field=models.ForeignKey( 21 | null=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | to=settings.AUTH_USER_MODEL, 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /api/migrations/0011_auto_20200611_1029.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 08:29 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("api", "0010_auto_20200611_1027"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="elo", 16 | name="player", 17 | field=models.ForeignKey( 18 | null=True, on_delete=django.db.models.deletion.CASCADE, to="api.Player" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /api/migrations/0012_auto_20200611_1101.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 09:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0011_auto_20200611_1029"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField(model_name="board", old_name="layout", new_name="fen",), 14 | ] 15 | -------------------------------------------------------------------------------- /api/migrations/0013_board_game_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 09:32 2 | 3 | import uuid 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("api", "0012_auto_20200611_1101"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="board", 17 | name="game_uuid", 18 | field=models.UUIDField(default=uuid.uuid4), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /api/migrations/0014_auto_20200611_1750.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 15:50 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("api", "0013_board_game_uuid"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="game", 16 | name="board", 17 | field=models.OneToOneField( 18 | on_delete=django.db.models.deletion.CASCADE, to="api.Board" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /api/migrations/0015_remove_player_guest.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-11 16:31 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0014_auto_20200611_1750"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField(model_name="player", name="guest",), 14 | ] 15 | -------------------------------------------------------------------------------- /api/migrations/0016_auto_20200614_1012.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-14 08:12 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ("api", "0015_remove_player_guest"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="claimitem", 18 | name="player", 19 | field=models.ForeignKey( 20 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="elo", 25 | name="player", 26 | field=models.ForeignKey( 27 | null=True, 28 | on_delete=django.db.models.deletion.CASCADE, 29 | to=settings.AUTH_USER_MODEL, 30 | ), 31 | ), 32 | migrations.AlterField( 33 | model_name="game", 34 | name="blacks_player", 35 | field=models.ForeignKey( 36 | null=True, 37 | on_delete=django.db.models.deletion.CASCADE, 38 | related_name="blacks_player", 39 | to=settings.AUTH_USER_MODEL, 40 | ), 41 | ), 42 | migrations.AlterField( 43 | model_name="game", 44 | name="whites_player", 45 | field=models.ForeignKey( 46 | null=True, 47 | on_delete=django.db.models.deletion.CASCADE, 48 | related_name="whites_player", 49 | to=settings.AUTH_USER_MODEL, 50 | ), 51 | ), 52 | migrations.DeleteModel(name="Player",), 53 | ] 54 | -------------------------------------------------------------------------------- /api/migrations/0017_auto_20200614_1133.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-14 09:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0016_auto_20200614_1012"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="game", old_name="start_timestamp", new_name="created_at", 15 | ), 16 | migrations.RenameField( 17 | model_name="game", old_name="end_timestamp", new_name="finished_at", 18 | ), 19 | migrations.AddField( 20 | model_name="game", name="started_at", field=models.DateTimeField(null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /api/migrations/0018_auto_20200615_1345.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-15 11:45 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("api", "0017_auto_20200614_1133"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="game", 16 | name="result", 17 | field=models.OneToOneField( 18 | on_delete=django.db.models.deletion.CASCADE, to="api.Result" 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /api/migrations/0019_auto_20200615_1843.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-15 16:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0018_auto_20200615_1345"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="result", 15 | name="description", 16 | field=models.TextField( 17 | choices=[ 18 | ("Abandoned", "Abandoned game."), 19 | ("Adjudication", "Result due to third party adjudication process."), 20 | ( 21 | "Death", 22 | "One or both players died during the course of this game.", 23 | ), 24 | ("Emergency", "Game concluded due to unforeseen circumstances."), 25 | ("Normal", "Game terminated in a normal fashion."), 26 | ( 27 | "Rules infraction", 28 | "Administrative forfeit due to losing player's failure to observe either the Laws of Chess or the event regulations.", 29 | ), 30 | ( 31 | "Time forfeit", 32 | "Loss due to losing player's failure to meet time control requirements.", 33 | ), 34 | ("Unterminated", "Game not terminated."), 35 | ], 36 | default="Unterminated", 37 | ), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /api/migrations/0020_auto_20200615_1851.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-15 16:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0019_auto_20200615_1843"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="result", old_name="description", new_name="termination", 15 | ), 16 | migrations.AddField( 17 | model_name="result", 18 | name="result", 19 | field=models.TextField( 20 | choices=[ 21 | ("White wins", "White wins"), 22 | ("Black wins", "Black wins"), 23 | ("Draw", "Drawn game"), 24 | ( 25 | "In progress", 26 | "Game still in progress, game abandoned, or result otherwise unknown", 27 | ), 28 | ], 29 | default="In progress", 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /api/migrations/0021_piece_square.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-17 17:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0020_auto_20200615_1851"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="piece", 15 | name="square", 16 | field=models.CharField( 17 | choices=[ 18 | (0, "A1"), 19 | (1, "B1"), 20 | (2, "C1"), 21 | (3, "D1"), 22 | (4, "E1"), 23 | (5, "F1"), 24 | (6, "G1"), 25 | (7, "H1"), 26 | (8, "A2"), 27 | (9, "B2"), 28 | (10, "C2"), 29 | (11, "D2"), 30 | (12, "E2"), 31 | (13, "F2"), 32 | (14, "G2"), 33 | (15, "H2"), 34 | (16, "A3"), 35 | (17, "B3"), 36 | (18, "C3"), 37 | (19, "D3"), 38 | (20, "E3"), 39 | (21, "F3"), 40 | (22, "G3"), 41 | (23, "H3"), 42 | (24, "A4"), 43 | (25, "B4"), 44 | (26, "C4"), 45 | (27, "D4"), 46 | (28, "E4"), 47 | (29, "F4"), 48 | (30, "G4"), 49 | (31, "H4"), 50 | (32, "A5"), 51 | (33, "B5"), 52 | (34, "C5"), 53 | (35, "D5"), 54 | (36, "E5"), 55 | (37, "F5"), 56 | (38, "G5"), 57 | (39, "H5"), 58 | (40, "A6"), 59 | (41, "B6"), 60 | (42, "C6"), 61 | (43, "D6"), 62 | (44, "E6"), 63 | (45, "F6"), 64 | (46, "G6"), 65 | (47, "H6"), 66 | (48, "A7"), 67 | (49, "B7"), 68 | (50, "C7"), 69 | (51, "D7"), 70 | (52, "E7"), 71 | (53, "F7"), 72 | (54, "G7"), 73 | (55, "H7"), 74 | (56, "A8"), 75 | (57, "B8"), 76 | (58, "C8"), 77 | (59, "D8"), 78 | (60, "E8"), 79 | (61, "F8"), 80 | (62, "G8"), 81 | (63, "H8"), 82 | ], 83 | max_length=2, 84 | null=True, 85 | ), 86 | ), 87 | ] 88 | -------------------------------------------------------------------------------- /api/migrations/0022_auto_20200618_1134.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 09:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0021_piece_square"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="board", 15 | name="castling_rights", 16 | field=models.TextField(null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="board", 20 | name="ep_square", 21 | field=models.CharField(max_length=2, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name="board", 25 | name="fullmove_number", 26 | field=models.IntegerField(default=1), 27 | ), 28 | migrations.AddField( 29 | model_name="board", 30 | name="halfmove_clock", 31 | field=models.IntegerField(default=0), 32 | ), 33 | migrations.AddField( 34 | model_name="board", name="turn", field=models.IntegerField(null=True), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /api/migrations/0023_auto_20200618_1152.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 09:52 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0022_auto_20200618_1134"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="board", old_name="castling_rights", new_name="castling_xfen", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /api/migrations/0024_board_castling_rights.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 10:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0023_auto_20200618_1152"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="board", 15 | name="castling_rights", 16 | field=models.TextField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /api/migrations/0025_auto_20200618_1319.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 11:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("api", "0024_board_castling_rights"), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField(model_name="move", name="piece",), 15 | migrations.AddField( 16 | model_name="move", 17 | name="board", 18 | field=models.ForeignKey( 19 | null=True, on_delete=django.db.models.deletion.CASCADE, to="api.Board" 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /api/migrations/0026_auto_20200618_1400.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 12:00 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0025_auto_20200618_1319"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="move", options={"ordering": ["created_at"]}, 15 | ), 16 | migrations.RenameField( 17 | model_name="move", old_name="timestamp", new_name="created_at", 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /api/migrations/0027_auto_20200618_1422.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 12:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0026_auto_20200618_1400"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", name="ep_square", field=models.IntegerField(null=True), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /api/migrations/0028_auto_20200618_1422.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 12:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0027_auto_20200618_1422"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="castling_rights", 16 | field=models.IntegerField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /api/migrations/0029_auto_20200618_1426.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 12:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0028_auto_20200618_1422"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", name="ep_square", field=models.IntegerField(default=0), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /api/migrations/0030_auto_20200618_1434.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 12:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0029_auto_20200618_1426"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="castling_rights", 16 | field=models.TextField(null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="board", name="ep_square", field=models.IntegerField(null=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /api/migrations/0031_auto_20200618_1636.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-18 14:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0030_auto_20200618_1434"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", name="turn", field=models.BooleanField(default=True), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /api/migrations/0032_board_board_fen.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-23 14:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0031_auto_20200618_1636"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="board", 15 | name="board_fen", 16 | field=models.TextField( 17 | default="rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /api/migrations/0033_auto_20200625_1202.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-25 10:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0032_board_board_fen"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="board_fen", 16 | field=models.TextField( 17 | default="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /api/migrations/0034_board_board_fen_flipped.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-15 06:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0033_auto_20200625_1202"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="board", 15 | name="board_fen_flipped", 16 | field=models.TextField( 17 | default="RNBQKBNR/PPPPPPPP/8/8/8/8/pppppppp/rnbqkbnr" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /api/migrations/0035_auto_20200715_1212.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-15 10:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0034_board_board_fen_flipped"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="board", 15 | name="board_fen_flipped", 16 | field=models.TextField( 17 | default="RNBKQBNR/PPPPPPPP/8/8/8/8/pppppppp/rnbkqbnr" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /api/migrations/0036_auto_20200716_1021.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-16 08:21 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 | ("api", "0035_auto_20200715_1212"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name="elo", 18 | name="player", 19 | field=models.OneToOneField( 20 | null=True, 21 | on_delete=django.db.models.deletion.CASCADE, 22 | related_name="elo", 23 | to=settings.AUTH_USER_MODEL, 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /api/migrations/0037_auto_20200716_1115.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-16 09:15 2 | 3 | import annoying.fields 4 | from django.conf import settings 5 | from django.db import migrations 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ("api", "0036_auto_20200716_1021"), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name="elo", 19 | name="player", 20 | field=annoying.fields.AutoOneToOneField( 21 | null=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | related_name="elo", 24 | to=settings.AUTH_USER_MODEL, 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /api/migrations/0038_remove_elo_k_factor.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-16 13:30 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0037_auto_20200716_1115"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField(model_name="elo", name="k_factor",), 14 | ] 15 | -------------------------------------------------------------------------------- /api/migrations/0039_elo_previous_rating.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-16 15:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("api", "0038_remove_elo_k_factor"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="elo", 15 | name="previous_rating", 16 | field=models.IntegerField(default=1200), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /api/migrations/0040_elo_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-17 07:34 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("api", "0039_elo_previous_rating"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="elo", name="uuid", field=models.UUIDField(default=uuid.uuid4), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /api/migrations/0041_auto_20200721_1015.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-07-21 08:15 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 | ("api", "0040_elo_uuid"), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField(model_name="game", name="blacks_player",), 17 | migrations.RemoveField(model_name="game", name="whites_player",), 18 | migrations.AddField( 19 | model_name="game", 20 | name="black_player", 21 | field=models.ForeignKey( 22 | null=True, 23 | on_delete=django.db.models.deletion.CASCADE, 24 | related_name="black_player", 25 | to=settings.AUTH_USER_MODEL, 26 | ), 27 | ), 28 | migrations.AddField( 29 | model_name="game", 30 | name="white_player", 31 | field=models.ForeignKey( 32 | null=True, 33 | on_delete=django.db.models.deletion.CASCADE, 34 | related_name="white_player", 35 | to=settings.AUTH_USER_MODEL, 36 | ), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/api/migrations/__init__.py -------------------------------------------------------------------------------- /api/permissions.py: -------------------------------------------------------------------------------- 1 | """ 2 | API permissions for checks such as piece ownership 3 | """ 4 | 5 | import chess 6 | from django.contrib.auth import get_user_model 7 | from rest_framework import permissions 8 | 9 | User = get_user_model() 10 | 11 | 12 | class GamePermission(permissions.BasePermission): 13 | message = "That move is not valid or allowed" 14 | 15 | def has_object_permission(self, request, view, obj): 16 | """ 17 | Allow players to see each other's pieces 18 | Only allow the owner of a piece to move it (i.e. if you play as White you can only move white pieces) 19 | 20 | python-chess Piece.color is True for white pieces, False for black ones 21 | obj: Game instance 22 | """ 23 | 24 | if request.method in permissions.SAFE_METHODS: 25 | return True 26 | 27 | if view.action == "move": 28 | from_square = request.data.get("from_square") 29 | square = getattr(chess, from_square.upper()) 30 | board = chess.Board(obj.board.fen) 31 | user = User.objects.get(username=request.user) 32 | 33 | if user in (obj.white_player, obj.black_player): 34 | if obj.white_player == obj.black_player: 35 | return True 36 | 37 | player_color = "white" if user == obj.white_player else "black" 38 | 39 | else: 40 | return False 41 | 42 | piece_color = "white" if board.color_at(square) else "black" 43 | 44 | return player_color == piece_color 45 | 46 | return True 47 | -------------------------------------------------------------------------------- /api/pgn_games/fools_mate.pgn: -------------------------------------------------------------------------------- 1 | [Event "UKR-ch U08"] 2 | [Site "Evpatoria"] 3 | [Date "2007.05.12"] 4 | [EventDate "2007.05.09"] 5 | [Round "4"] 6 | [Result "0-1"] 7 | [White "Ivan Skrypin"] 8 | [Black "Alexey Glebov"] 9 | [ECO "A02"] 10 | [WhiteElo "?"] 11 | [BlackElo "?"] 12 | [PlyCount "4"] 13 | 14 | 1. f4 e6 2. g4 Qh4# 0-1 -------------------------------------------------------------------------------- /api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import serializers 3 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 4 | 5 | from . import services 6 | from .models import Board, Elo, Game, Result 7 | 8 | 9 | class EloSerializer(serializers.ModelSerializer): 10 | class Meta: 11 | model = Elo 12 | fields = ( 13 | "rating", 14 | "previous_rating", 15 | "wins", 16 | "losses", 17 | "draws", 18 | "uuid", 19 | ) 20 | 21 | 22 | class UserEloSerializer(serializers.ModelSerializer): 23 | elo = EloSerializer() 24 | 25 | class Meta: 26 | model = get_user_model() 27 | fields = ( 28 | "username", 29 | "active", 30 | "elo", 31 | ) 32 | 33 | 34 | class BoardSerializer(serializers.ModelSerializer): 35 | class Meta: 36 | model = Board 37 | fields = ( 38 | "fen", 39 | "board_fen", 40 | "board_fen_flipped", 41 | "updated_at", 42 | "game_uuid", 43 | ) 44 | 45 | 46 | class ResultSerializer(serializers.ModelSerializer): 47 | class Meta: 48 | model = Result 49 | fields = ( 50 | "result", 51 | "termination", 52 | ) 53 | 54 | 55 | class GameSerializer(serializers.ModelSerializer): 56 | black_player = UserEloSerializer(required=False) 57 | white_player = UserEloSerializer(required=False) 58 | board = BoardSerializer(required=False) 59 | result = ResultSerializer(required=False) 60 | 61 | class Meta: 62 | model = Game 63 | fields = ( 64 | "uuid", 65 | "black_player", 66 | "white_player", 67 | "created_at", 68 | "started_at", 69 | "finished_at", 70 | "board", 71 | "result", 72 | ) 73 | 74 | def create(self, validated_data): 75 | result_data = validated_data.pop("result", {}) 76 | board_data = validated_data.pop("board", {}) 77 | 78 | preferred_color = self.context["request"].data.get("preferred_color", "random") 79 | 80 | auth_username = self.context["request"].user 81 | 82 | game = services.create_game( 83 | result_data=result_data, board_data=board_data, **validated_data 84 | ) 85 | 86 | services.assign_color(game, auth_username, preferred_color) 87 | 88 | return game 89 | 90 | 91 | class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): 92 | def validate(self, attrs): 93 | data = super().validate(attrs) 94 | 95 | data["name"] = self.user.username 96 | data["active"] = self.user.active 97 | 98 | return data 99 | -------------------------------------------------------------------------------- /api/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import chess 3 | 4 | from api import services 5 | from api.models import Board, Game, Result 6 | 7 | 8 | JWT_TOKEN = { 9 | "refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU5NTIzMjMxMCwianRpIjoiMTViM2ZiZGNhODJlNDBiMDkyNTBiYzA5ZTlkODQwMmYiLCJ1c2VyX2lkIjoxfQ.3UZIhcS4X14zb9V7wRnf0G3TJ1f7G6UMijThokvOD_M", 10 | "access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTk1MTQ1OTIyLCJqdGkiOiI1ODBjNmM3ZDcyNTk0NmJmOWJiNGY1YmMyMzcyMjY0MiIsInVzZXJfaWQiOjF9.44RKjIbKXmWqjzJA0TtpbnBTt3-3tAMxUP1EZDMJais", 11 | } 12 | 13 | CHESS_BOARD = chess.Board() 14 | BOARD_INSTANCE = Board.from_fen(CHESS_BOARD.fen()) 15 | 16 | 17 | @pytest.fixture 18 | def users(): 19 | user_one = services.User.objects.create( 20 | username="walterwhite", 21 | name="Walter Hartwell White", 22 | email="walter@graymatter.tech", 23 | password="albuquerque1992", 24 | is_active=True, 25 | ) 26 | 27 | user_two = services.User.objects.create( 28 | username="jessepinkman", 29 | name="Jesse Bruce Pinkman", 30 | email="jesseyo@outlook.com", 31 | password="therealog", 32 | is_active=True, 33 | ) 34 | 35 | return user_one, user_two 36 | 37 | 38 | @pytest.fixture 39 | def game_instance(users): 40 | player, opponent = users 41 | 42 | return Game( 43 | board=BOARD_INSTANCE, 44 | white_player=player, 45 | black_player=opponent, 46 | result=Result(result=Result.BLACK_WINS, termination=Result.NORMAL), 47 | ) 48 | -------------------------------------------------------------------------------- /api/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import chess 3 | 4 | from api.models import Board 5 | 6 | 7 | def test_board_from_fen(): 8 | chess_board = chess.Board() 9 | fen = chess_board.fen() 10 | board = Board.from_fen(fen) 11 | 12 | assert board.ep_square is None 13 | assert board.halfmove_clock == 0 14 | assert board.fullmove_number == 1 15 | assert board.castling_xfen == "KQkq" 16 | 17 | 18 | @pytest.mark.django_db 19 | def test_get_empty_move_stack(): 20 | board = Board() 21 | 22 | assert board.move_stack == [] 23 | 24 | 25 | @pytest.mark.django_db 26 | def test_board_model_string(): 27 | board = Board() 28 | assert str(board) == board.fen 29 | -------------------------------------------------------------------------------- /api/tests/test_services.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import chess 4 | import chess.pgn 5 | import pytest 6 | from api import services 7 | from api.models import Board, Game, Result 8 | from fixtures import users, game_instance 9 | 10 | 11 | @pytest.mark.django_db 12 | def test_chess_board_from_uuid(): 13 | """ 14 | Test a python-chess Board can be fully recovered from a Board model instance 15 | by providing the game uuid 16 | """ 17 | 18 | # Create board and make two moves 19 | chess_board = chess.Board() 20 | board_instance = Board.from_fen(chess_board.fen()) 21 | board_instance.save() 22 | 23 | first_move = "e2e4" 24 | second_move = "e7e5" 25 | 26 | services.move_piece( 27 | board_instance, first_move[:2], first_move[2:], chess_board=chess_board 28 | ) 29 | services.move_piece( 30 | board_instance, second_move[:2], second_move[2:], chess_board=chess_board 31 | ) 32 | 33 | new_chess_board = services.chess_board_from_uuid(board_instance.game_uuid) 34 | 35 | assert chess_board.ep_square == int(new_chess_board.ep_square) 36 | assert chess_board.castling_rights == int(new_chess_board.castling_rights) 37 | assert chess_board.turn == new_chess_board.turn 38 | assert chess_board.fullmove_number == new_chess_board.fullmove_number 39 | assert chess_board.halfmove_clock == new_chess_board.halfmove_clock 40 | assert chess_board.move_stack == new_chess_board.move_stack 41 | assert chess_board.fen() == new_chess_board.fen() 42 | assert chess_board.board_fen() == new_chess_board.board_fen() 43 | 44 | 45 | @pytest.mark.django_db 46 | def test_create_board_from_pgn(): 47 | board_instance, chess_board = services.create_board_from_pgn( 48 | "api/pgn_games/fools_mate.pgn", starting_at=0 49 | ) 50 | 51 | assert board_instance.fen == chess.STARTING_FEN 52 | assert chess_board.fen() == chess.STARTING_FEN 53 | 54 | 55 | @pytest.mark.django_db 56 | def test_create_board_from_pgn_starting_at(): 57 | board_instance, chess_board = services.create_board_from_pgn( 58 | "api/pgn_games/fools_mate.pgn", starting_at=4 59 | ) 60 | 61 | assert board_instance.fullmove_number == 3 62 | assert chess_board.is_checkmate() 63 | 64 | 65 | @pytest.mark.django_db 66 | def test_get_expected_score(): 67 | player_rating = 1200 68 | opponent_rating = 1300 69 | 70 | expected_player_score = services._get_expected_score(player_rating, opponent_rating) 71 | 72 | expected_opponent_score = services._get_expected_score( 73 | opponent_rating, player_rating 74 | ) 75 | 76 | assert expected_player_score == 0.36 77 | assert expected_opponent_score == 0.64 78 | 79 | 80 | @pytest.mark.django_db 81 | @mock.patch("api.services.K_FACTOR", 32) 82 | def test_get_rating(users): 83 | player, opponent = users 84 | 85 | rating = services.get_rating(1, player.elo.rating, opponent.elo.rating) 86 | 87 | assert rating == 1216 88 | 89 | 90 | @pytest.mark.django_db 91 | @mock.patch("api.services.K_FACTOR", 32) 92 | def test_update_elo_rating(users): 93 | player, opponent = users 94 | 95 | new_rating = services.update_elo_rating( 96 | player_score=1, player=player, opponent=opponent 97 | ) 98 | 99 | assert new_rating == 1216 100 | assert player.elo.rating == 1216 101 | 102 | 103 | @pytest.mark.django_db 104 | def test_update_elo(users): 105 | """ 106 | Test ELO is recalculated correctly 107 | The default result is Result(result=Result.WHITE_WINS, termination=Result.NORMAL) 108 | """ 109 | 110 | player, opponent = users 111 | 112 | board_instance, _ = services.create_board_from_pgn( 113 | "api/pgn_games/fools_mate.pgn", starting_at=4 114 | ) 115 | 116 | game = Game( 117 | board=board_instance, 118 | white_player=player, 119 | black_player=opponent, 120 | result=Result(result=Result.BLACK_WINS, termination=Result.NORMAL), 121 | ) 122 | 123 | services.update_elo(game) 124 | 125 | assert player.elo.wins == 0 126 | assert opponent.elo.wins == 1 127 | 128 | assert player.elo.rating == 1184 129 | assert opponent.elo.rating == 1216 130 | 131 | 132 | @pytest.mark.django_db 133 | def test_update_elo_draw(users, game_instance): 134 | """ 135 | game.result = Result(result=Result.DRAW, termination=Result.NORMAL) 136 | """ 137 | 138 | player, opponent = users 139 | game = game_instance 140 | 141 | game.result = Result(result=Result.DRAW) 142 | game.result.save() 143 | 144 | services.update_elo(game) 145 | 146 | assert player.elo.draws == 1 147 | assert opponent.elo.draws == 1 148 | 149 | 150 | @pytest.mark.django_db 151 | def test_update_elo_white_wins(users, game_instance): 152 | """ 153 | game.result = Result(result=Result.WHITE_WINS, termination=Result.NORMAL) 154 | """ 155 | 156 | player, opponent = users 157 | game = game_instance 158 | 159 | game.result = Result(result=Result.WHITE_WINS) 160 | game.result.save() 161 | 162 | services.update_elo(game) 163 | 164 | assert player.elo.wins == 1 165 | assert opponent.elo.wins == 0 166 | -------------------------------------------------------------------------------- /api/tests/test_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 3 | 4 | from api.serializers import CustomTokenObtainPairSerializer 5 | 6 | from unittest.mock import MagicMock 7 | 8 | from fixtures import JWT_TOKEN, users 9 | 10 | 11 | TokenObtainPairSerializer.validate = MagicMock(return_value=JWT_TOKEN) 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_custom_jwt_token(users): 16 | user_one, _ = users 17 | 18 | custom_token_serializer = CustomTokenObtainPairSerializer() 19 | custom_token_serializer.user = user_one 20 | 21 | data = custom_token_serializer.validate("foo") 22 | 23 | assert data.get("refresh", "") == JWT_TOKEN["refresh"] 24 | assert data.get("access", "") == JWT_TOKEN["access"] 25 | assert data.get("name", "") == user_one.username 26 | assert data.get("active", "") == user_one.active 27 | -------------------------------------------------------------------------------- /api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db.models import Q 3 | from django.shortcuts import get_object_or_404 4 | from rest_framework import mixins, status, viewsets 5 | from rest_framework.decorators import action 6 | from rest_framework.response import Response 7 | from rest_framework_simplejwt.views import TokenObtainPairView 8 | 9 | from . import services 10 | from .models import Elo, Game 11 | from .permissions import GamePermission 12 | from .serializers import CustomTokenObtainPairSerializer, EloSerializer, GameSerializer 13 | 14 | User = get_user_model() 15 | 16 | 17 | class GameViewSet(viewsets.ModelViewSet): 18 | serializer_class = GameSerializer 19 | queryset = Game.objects.all().order_by("-created_at") 20 | 21 | permission_classes = [ 22 | GamePermission, 23 | ] 24 | 25 | @action(detail=True, methods=["put"]) 26 | def move(self, request, *args, **kwargs): 27 | """ 28 | Move a piece 29 | """ 30 | 31 | from_square = request.data.get("from_square") 32 | to_square = request.data.get("to_square") 33 | 34 | game_uuid = kwargs.get("pk") 35 | 36 | game = get_object_or_404(Game, uuid=game_uuid) 37 | 38 | board = game.board 39 | auth_user = User.objects.get(username=request.user) 40 | 41 | self.check_object_permissions(self.request, game) 42 | 43 | move = services.move_piece(board, from_square, to_square) 44 | 45 | if move: 46 | return Response(self.serializer_class(game).data) 47 | 48 | else: 49 | return Response( 50 | data={"detail": f"{from_square}{to_square} is not a valid move."}, 51 | status=status.HTTP_400_BAD_REQUEST, 52 | ) 53 | 54 | @action(detail=True, methods=["put"]) 55 | def join(self, request, *args, **kwargs): 56 | """ 57 | Join a game 58 | """ 59 | 60 | game_uuid = kwargs.get("pk") 61 | preferred_color = request.data.get("preferred_color") 62 | 63 | game = get_object_or_404(Game, uuid=game_uuid) 64 | 65 | services.assign_color(game, request.user, preferred_color=preferred_color) 66 | 67 | serialized_game = GameSerializer(game).data 68 | 69 | return Response(data=serialized_game) 70 | 71 | @action(detail=False, methods=["get"]) 72 | def get_unfinished_games(self, request, *args, **kwargs): 73 | """ 74 | Get a list of unfinished games played by the user 75 | """ 76 | 77 | user = self.request.user 78 | games = Game.objects.filter( 79 | Q(white_player=user) | Q(black_player=user) 80 | ).order_by("-created_at") 81 | 82 | page = self.paginate_queryset(games) 83 | serialized_games = self.get_serializer(page, many=True).data 84 | 85 | return ( 86 | self.get_paginated_response(serialized_games) 87 | if page 88 | else Response(data=serialized_games) 89 | ) 90 | 91 | 92 | class EloViewSet( 93 | mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet 94 | ): 95 | queryset = Elo.objects.all() 96 | serializer_class = EloSerializer 97 | lookup_field = "uuid" 98 | 99 | 100 | class CustomTokenObtainPairView(TokenObtainPairView): 101 | serializer_class = CustomTokenObtainPairSerializer 102 | -------------------------------------------------------------------------------- /chess_api_project/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple( 3 | [ 4 | int(num) if num.isdigit() else num 5 | for num in __version__.replace("-", ".", 1).split(".") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /chess_api_project/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from chess_api_project.users.models import User 4 | from chess_api_project.users.tests.factories import UserFactory 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def media_storage(settings, tmpdir): 9 | settings.MEDIA_ROOT = tmpdir.strpath 10 | 11 | 12 | @pytest.fixture 13 | def user() -> User: 14 | return UserFactory() 15 | -------------------------------------------------------------------------------- /chess_api_project/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /chess_api_project/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /chess_api_project/contrib/sites/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.contrib.sites.models import _simple_domain_name_validator 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Site", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ( 24 | "domain", 25 | models.CharField( 26 | max_length=100, 27 | verbose_name="domain name", 28 | validators=[_simple_domain_name_validator], 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50, verbose_name="display name")), 32 | ], 33 | options={ 34 | "ordering": ("domain",), 35 | "db_table": "django_site", 36 | "verbose_name": "site", 37 | "verbose_name_plural": "sites", 38 | }, 39 | bases=(models.Model,), 40 | managers=[("objects", django.contrib.sites.models.SiteManager())], 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /chess_api_project/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /chess_api_project/contrib/sites/migrations/0003_set_site_domain_and_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | from django.conf import settings 7 | from django.db import migrations 8 | 9 | 10 | def update_site_forward(apps, schema_editor): 11 | """Set site domain and name.""" 12 | Site = apps.get_model("sites", "Site") 13 | Site.objects.update_or_create( 14 | id=settings.SITE_ID, 15 | defaults={"domain": "fullstackdjango.com", "name": "Django Chess API",}, 16 | ) 17 | 18 | 19 | def update_site_backward(apps, schema_editor): 20 | """Revert site domain and name to default.""" 21 | Site = apps.get_model("sites", "Site") 22 | Site.objects.update_or_create( 23 | id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} 24 | ) 25 | 26 | 27 | class Migration(migrations.Migration): 28 | 29 | dependencies = [("sites", "0002_alter_domain_unique")] 30 | 31 | operations = [migrations.RunPython(update_site_forward, update_site_backward)] 32 | -------------------------------------------------------------------------------- /chess_api_project/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /chess_api_project/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | -------------------------------------------------------------------------------- /chess_api_project/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/static/fonts/.gitkeep -------------------------------------------------------------------------------- /chess_api_project/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /chess_api_project/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /chess_api_project/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/static/sass/custom_bootstrap_vars.scss -------------------------------------------------------------------------------- /chess_api_project/static/sass/project.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // project specific CSS goes here 6 | 7 | //////////////////////////////// 8 | //Variables// 9 | //////////////////////////////// 10 | 11 | // Alert colors 12 | 13 | $white: #fff; 14 | $mint-green: #d6e9c6; 15 | $black: #000; 16 | $pink: #f2dede; 17 | $dark-pink: #eed3d7; 18 | $red: #b94a48; 19 | 20 | //////////////////////////////// 21 | //Alerts// 22 | //////////////////////////////// 23 | 24 | // bootstrap alert CSS, translated to the django-standard levels of 25 | // debug, info, success, warning, error 26 | 27 | .alert-debug { 28 | background-color: $white; 29 | border-color: $mint-green; 30 | color: $black; 31 | } 32 | 33 | .alert-error { 34 | background-color: $pink; 35 | border-color: $dark-pink; 36 | color: $red; 37 | } 38 | -------------------------------------------------------------------------------- /chess_api_project/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

CSRF verification failed. Request aborted.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /chess_api_project/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

This is not the page you were looking for.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /chess_api_project/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Server Error{% endblock %} 4 | 5 | {% block content %} 6 |

Ooops!!! 500

7 | 8 |

Looks like something went wrong!

9 | 10 |

We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.

11 | {% endblock content %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Account Inactive" %}

9 | 10 |

{% trans "This account is inactive." %}

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% block inner %}{% endblock %} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/email.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "account/base.html" %} 3 | 4 | {% load i18n %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Account" %}{% endblock %} 8 | 9 | {% block inner %} 10 |

{% trans "E-mail Addresses" %}

11 | 12 | {% if user.emailaddress_set.all %} 13 |

{% trans 'The following e-mail addresses are associated with your account:' %}

14 | 15 |
16 | {% csrf_token %} 17 |
18 | 19 | {% for emailaddress in user.emailaddress_set.all %} 20 |
21 | 33 |
34 | {% endfor %} 35 | 36 |
37 | 38 | 39 | 40 |
41 | 42 |
43 |
44 | 45 | {% else %} 46 |

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

47 | 48 | {% endif %} 49 | 50 | 51 |

{% trans "Add E-mail Address" %}

52 | 53 |
54 | {% csrf_token %} 55 | {{ form|crispy }} 56 | 57 |
58 | 59 | {% endblock %} 60 | 61 | 62 | {% block javascript %} 63 | {{ block.super }} 64 | 79 | {% endblock %} 80 | 81 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 7 | 8 | 9 | {% block inner %} 10 |

{% trans "Confirm E-mail Address" %}

11 | 12 | {% if confirmation %} 13 | 14 | {% user_display confirmation.email_address.user as user_display %} 15 | 16 |

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | {% else %} 24 | 25 | {% url 'account_email' as email_url %} 26 | 27 |

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

28 | 29 | {% endif %} 30 | 31 | {% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account socialaccount %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Sign In" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% trans "Sign In" %}

12 | 13 | {% get_providers as socialaccount_providers %} 14 | 15 | {% if socialaccount_providers %} 16 |

{% blocktrans with site.name as site_name %}Please sign in with one 17 | of your existing third party accounts. Or, sign up 18 | for a {{ site_name }} account and sign in below:{% endblocktrans %}

19 | 20 |
21 | 22 | 25 | 26 |
{% trans 'or' %}
27 | 28 |
29 | 30 | {% include "socialaccount/snippets/login_extra.html" %} 31 | 32 | {% else %} 33 |

{% blocktrans %}If you have not created an account yet, then please 34 | sign up first.{% endblocktrans %}

35 | {% endif %} 36 | 37 |
38 | {% csrf_token %} 39 | {{ form|crispy }} 40 | {% if redirect_field_value %} 41 | 42 | {% endif %} 43 | {% trans "Forgot Password?" %} 44 | 45 |
46 | 47 | {% endblock %} 48 | 49 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Out" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Sign Out" %}

9 | 10 |

{% trans 'Are you sure you want to sign out?' %}

11 | 12 |
13 | {% csrf_token %} 14 | {% if redirect_field_value %} 15 | 16 | {% endif %} 17 | 18 |
19 | 20 | 21 | {% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Change Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% trans "Password Reset" %}

12 | {% if user.is_authenticated %} 13 | {% include "account/snippets/already_logged_in.html" %} 14 | {% endif %} 15 | 16 |

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

17 | 18 |
19 | {% csrf_token %} 20 | {{ form|crispy }} 21 | 22 |
23 | 24 |

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Password Reset" %}

10 | 11 | {% if user.is_authenticated %} 12 | {% include "account/snippets/already_logged_in.html" %} 13 | {% endif %} 14 | 15 |

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

9 | 10 | {% if token_fail %} 11 | {% url 'account_reset_password' as passwd_reset_url %} 12 |

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

13 | {% else %} 14 | {% if form %} 15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 |
20 | {% else %} 21 |

{% trans 'Your password is now changed.' %}

22 | {% endif %} 23 | {% endif %} 24 | {% endblock %} 25 | 26 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block inner %} 7 |

{% trans "Change Password" %}

8 |

{% trans 'Your password is now changed.' %}

9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Set Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Set Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Signup" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Sign Up" %}

10 | 11 |

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

12 | 13 |
14 | {% csrf_token %} 15 | {{ form|crispy }} 16 | {% if redirect_field_value %} 17 | 18 | {% endif %} 19 | 20 |
21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Sign Up Closed" %}

9 | 10 |

{% trans "We are sorry, but the sign up is currently closed." %}

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Verify Your E-mail Address" %}

9 | 10 |

{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

11 | 12 | {% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /chess_api_project/templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Verify Your E-mail Address" %}

9 | 10 | {% url 'account_email' as email_url %} 11 | 12 |

{% blocktrans %}This part of the site requires us to verify that 13 | you are who you claim to be. For this purpose, we require that you 14 | verify ownership of your e-mail address. {% endblocktrans %}

15 | 16 |

{% blocktrans %}We have sent an e-mail to you for 17 | verification. Please click on the link inside this e-mail. Please 18 | contact us if you do not receive it within a few minutes.{% endblocktrans %}

19 | 20 |

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

21 | 22 | 23 | {% endblock %} 24 | 25 | -------------------------------------------------------------------------------- /chess_api_project/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | {% block title %}Django Chess API{% endblock title %} 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | {% block css %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endblock %} 32 | 33 | 34 | 35 | 36 | 37 |
38 | 74 | 75 |
76 | 77 |
78 | 79 | {% if messages %} 80 | {% for message in messages %} 81 |
{{ message }}
82 | {% endfor %} 83 | {% endif %} 84 | 85 | {% block content %} 86 |

Use this document as a way to quick start any new project.

87 | {% endblock content %} 88 | 89 |
90 | 91 | {% block modal %}{% endblock modal %} 92 | 93 | 95 | 96 | {% block javascript %} 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {% endblock javascript %} 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /chess_api_project/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /chess_api_project/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /chess_api_project/templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}User: {{ object.username }}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |
10 |
11 | 12 |

{{ object.username }}

13 | {% if object.name %} 14 |

{{ object.name }}

15 | {% endif %} 16 |
17 |
18 | 19 | {% if object == request.user %} 20 | 21 |
22 | 23 |
24 | My Info 25 | E-Mail 26 | 27 |
28 | 29 |
30 | 31 | {% endif %} 32 | 33 | 34 |
35 | {% endblock content %} 36 | 37 | -------------------------------------------------------------------------------- /chess_api_project/templates/users/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block title %}{{ user.username }}{% endblock %} 5 | 6 | {% block content %} 7 |

{{ user.username }}

8 |
9 | {% csrf_token %} 10 | {{ form|crispy }} 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /chess_api_project/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/users/__init__.py -------------------------------------------------------------------------------- /chess_api_project/users/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 5 | from django.conf import settings 6 | from django.http import HttpRequest 7 | 8 | 9 | class AccountAdapter(DefaultAccountAdapter): 10 | def is_open_for_signup(self, request: HttpRequest): 11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 12 | 13 | 14 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): 16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 17 | -------------------------------------------------------------------------------- /chess_api_project/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import admin as auth_admin 3 | from django.contrib.auth import get_user_model 4 | 5 | from chess_api_project.users.forms import UserChangeForm, UserCreationForm 6 | 7 | User = get_user_model() 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(auth_admin.UserAdmin): 12 | 13 | form = UserChangeForm 14 | add_form = UserCreationForm 15 | fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets 16 | list_display = ["username", "name", "is_superuser"] 17 | search_fields = ["name"] 18 | -------------------------------------------------------------------------------- /chess_api_project/users/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from chess_api_project.users.models import User 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = User 9 | fields = ["username", "email", "name", "url"] 10 | 11 | extra_kwargs = { 12 | "url": {"view_name": "api:user-detail", "lookup_field": "username"} 13 | } 14 | -------------------------------------------------------------------------------- /chess_api_project/users/api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from rest_framework import status 3 | from rest_framework.decorators import action 4 | from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin 5 | from rest_framework.response import Response 6 | from rest_framework.viewsets import GenericViewSet 7 | 8 | from .serializers import UserSerializer 9 | 10 | User = get_user_model() 11 | 12 | 13 | class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): 14 | serializer_class = UserSerializer 15 | queryset = User.objects.all() 16 | lookup_field = "username" 17 | 18 | def get_queryset(self, *args, **kwargs): 19 | return self.queryset.filter(id=self.request.user.id) 20 | 21 | @action(detail=False, methods=["GET"]) 22 | def me(self, request): 23 | serializer = UserSerializer(request.user, context={"request": request}) 24 | return Response(status=status.HTTP_200_OK, data=serializer.data) 25 | -------------------------------------------------------------------------------- /chess_api_project/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class UsersConfig(AppConfig): 6 | name = "chess_api_project.users" 7 | verbose_name = _("Users") 8 | 9 | def ready(self): 10 | try: 11 | import chess_api_project.users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /chess_api_project/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import forms, get_user_model 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserChangeForm(forms.UserChangeForm): 9 | class Meta(forms.UserChangeForm.Meta): 10 | model = User 11 | 12 | 13 | class UserCreationForm(forms.UserCreationForm): 14 | 15 | error_message = forms.UserCreationForm.error_messages.update( 16 | {"duplicate_username": _("This username has already been taken.")} 17 | ) 18 | 19 | class Meta(forms.UserCreationForm.Meta): 20 | model = User 21 | 22 | def clean_username(self): 23 | username = self.cleaned_data["username"] 24 | 25 | try: 26 | User.objects.get(username=username) 27 | except User.DoesNotExist: 28 | return username 29 | 30 | raise ValidationError(self.error_messages["duplicate_username"]) 31 | -------------------------------------------------------------------------------- /chess_api_project/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.auth.models 2 | import django.contrib.auth.validators 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [("auth", "0008_alter_user_username_max_length")] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="User", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("password", models.CharField(max_length=128, verbose_name="password")), 27 | ( 28 | "last_login", 29 | models.DateTimeField( 30 | blank=True, null=True, verbose_name="last login" 31 | ), 32 | ), 33 | ( 34 | "is_superuser", 35 | models.BooleanField( 36 | default=False, 37 | help_text="Designates that this user has all permissions without explicitly assigning them.", 38 | verbose_name="superuser status", 39 | ), 40 | ), 41 | ( 42 | "username", 43 | models.CharField( 44 | error_messages={ 45 | "unique": "A user with that username already exists." 46 | }, 47 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 48 | max_length=150, 49 | unique=True, 50 | validators=[ 51 | django.contrib.auth.validators.UnicodeUsernameValidator() 52 | ], 53 | verbose_name="username", 54 | ), 55 | ), 56 | ( 57 | "first_name", 58 | models.CharField( 59 | blank=True, max_length=30, verbose_name="first name" 60 | ), 61 | ), 62 | ( 63 | "last_name", 64 | models.CharField( 65 | blank=True, max_length=150, verbose_name="last name" 66 | ), 67 | ), 68 | ( 69 | "email", 70 | models.EmailField( 71 | blank=True, max_length=254, verbose_name="email address" 72 | ), 73 | ), 74 | ( 75 | "is_staff", 76 | models.BooleanField( 77 | default=False, 78 | help_text="Designates whether the user can log into this admin site.", 79 | verbose_name="staff status", 80 | ), 81 | ), 82 | ( 83 | "is_active", 84 | models.BooleanField( 85 | default=True, 86 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 87 | verbose_name="active", 88 | ), 89 | ), 90 | ( 91 | "date_joined", 92 | models.DateTimeField( 93 | default=django.utils.timezone.now, verbose_name="date joined" 94 | ), 95 | ), 96 | ( 97 | "name", 98 | models.CharField( 99 | blank=True, max_length=255, verbose_name="Name of User" 100 | ), 101 | ), 102 | ( 103 | "groups", 104 | models.ManyToManyField( 105 | blank=True, 106 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 107 | related_name="user_set", 108 | related_query_name="user", 109 | to="auth.Group", 110 | verbose_name="groups", 111 | ), 112 | ), 113 | ( 114 | "user_permissions", 115 | models.ManyToManyField( 116 | blank=True, 117 | help_text="Specific permissions for this user.", 118 | related_name="user_set", 119 | related_query_name="user", 120 | to="auth.Permission", 121 | verbose_name="user permissions", 122 | ), 123 | ), 124 | ], 125 | options={ 126 | "verbose_name_plural": "users", 127 | "verbose_name": "user", 128 | "abstract": False, 129 | }, 130 | managers=[("objects", django.contrib.auth.models.UserManager())], 131 | ) 132 | ] 133 | -------------------------------------------------------------------------------- /chess_api_project/users/migrations/0002_user_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 09:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("users", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="user", name="active", field=models.BooleanField(default=True), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /chess_api_project/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/users/migrations/__init__.py -------------------------------------------------------------------------------- /chess_api_project/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db.models import CharField, BooleanField 3 | from django.urls import reverse 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class User(AbstractUser): 8 | 9 | # First Name and Last Name do not cover name patterns 10 | # around the globe. 11 | name = CharField(_("Name of User"), blank=True, max_length=255) 12 | active = BooleanField(default=True) 13 | 14 | def get_absolute_url(self): 15 | return reverse("users:detail", kwargs={"username": self.username}) 16 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/users/tests/__init__.py -------------------------------------------------------------------------------- /chess_api_project/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | from django.contrib.auth import get_user_model 4 | from factory import DjangoModelFactory, Faker, post_generation 5 | 6 | 7 | class UserFactory(DjangoModelFactory): 8 | 9 | username = Faker("user_name") 10 | email = Faker("email") 11 | name = Faker("name") 12 | 13 | @post_generation 14 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 15 | password = ( 16 | extracted 17 | if extracted 18 | else Faker( 19 | "password", 20 | length=42, 21 | special_chars=True, 22 | digits=True, 23 | upper_case=True, 24 | lower_case=True, 25 | ).generate(extra_kwargs={}) 26 | ) 27 | self.set_password(password) 28 | 29 | class Meta: 30 | model = get_user_model() 31 | django_get_or_create = ["username"] 32 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/test_drf_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from chess_api_project.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_user_detail(user: User): 10 | assert ( 11 | reverse("api:user-detail", kwargs={"username": user.username}) 12 | == f"/api/users/{user.username}/" 13 | ) 14 | assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" 15 | 16 | 17 | def test_user_list(): 18 | assert reverse("api:user-list") == "/api/users/" 19 | assert resolve("/api/users/").view_name == "api:user-list" 20 | 21 | 22 | def test_user_me(): 23 | assert reverse("api:user-me") == "/api/users/me/" 24 | assert resolve("/api/users/me/").view_name == "api:user-me" 25 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/test_drf_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import RequestFactory 3 | 4 | from chess_api_project.users.api.views import UserViewSet 5 | from chess_api_project.users.models import User 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | class TestUserViewSet: 11 | def test_get_queryset(self, user: User, rf: RequestFactory): 12 | view = UserViewSet() 13 | request = rf.get("/fake-url/") 14 | request.user = user 15 | 16 | view.request = request 17 | 18 | assert user in view.get_queryset() 19 | 20 | def test_me(self, user: User, rf: RequestFactory): 21 | view = UserViewSet() 22 | request = rf.get("/fake-url/") 23 | request.user = user 24 | 25 | view.request = request 26 | 27 | response = view.me(request) 28 | 29 | assert response.data == { 30 | "username": user.username, 31 | "email": user.email, 32 | "name": user.name, 33 | "url": f"http://testserver/api/users/{user.username}/", 34 | } 35 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from chess_api_project.users.forms import UserCreationForm 4 | from chess_api_project.users.tests.factories import UserFactory 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | class TestUserCreationForm: 10 | def test_clean_username(self): 11 | # A user with proto_user params does not exist yet. 12 | proto_user = UserFactory.build() 13 | 14 | form = UserCreationForm( 15 | { 16 | "username": proto_user.username, 17 | "password1": proto_user._password, 18 | "password2": proto_user._password, 19 | } 20 | ) 21 | 22 | assert form.is_valid() 23 | assert form.clean_username() == proto_user.username 24 | 25 | # Creating a user. 26 | form.save() 27 | 28 | # The user with proto_user params already exists, 29 | # hence cannot be created. 30 | form = UserCreationForm( 31 | { 32 | "username": proto_user.username, 33 | "password1": proto_user._password, 34 | "password2": proto_user._password, 35 | } 36 | ) 37 | 38 | assert not form.is_valid() 39 | assert len(form.errors) == 1 40 | assert "username" in form.errors 41 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from chess_api_project.users.models import User 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_user_get_absolute_url(user: User): 9 | assert user.get_absolute_url() == f"/users/{user.username}/" 10 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import resolve, reverse 3 | 4 | from chess_api_project.users.models import User 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | def test_detail(user: User): 10 | assert ( 11 | reverse("users:detail", kwargs={"username": user.username}) 12 | == f"/users/{user.username}/" 13 | ) 14 | assert resolve(f"/users/{user.username}/").view_name == "users:detail" 15 | 16 | 17 | def test_update(): 18 | assert reverse("users:update") == "/users/~update/" 19 | assert resolve("/users/~update/").view_name == "users:update" 20 | 21 | 22 | def test_redirect(): 23 | assert reverse("users:redirect") == "/users/~redirect/" 24 | assert resolve("/users/~redirect/").view_name == "users:redirect" 25 | -------------------------------------------------------------------------------- /chess_api_project/users/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth.models import AnonymousUser 3 | from django.http.response import Http404 4 | from django.test import RequestFactory 5 | 6 | from chess_api_project.users.models import User 7 | from chess_api_project.users.tests.factories import UserFactory 8 | from chess_api_project.users.views import ( 9 | UserRedirectView, 10 | UserUpdateView, 11 | user_detail_view, 12 | ) 13 | 14 | pytestmark = pytest.mark.django_db 15 | 16 | 17 | class TestUserUpdateView: 18 | """ 19 | TODO: 20 | extracting view initialization code as class-scoped fixture 21 | would be great if only pytest-django supported non-function-scoped 22 | fixture db access -- this is a work-in-progress for now: 23 | https://github.com/pytest-dev/pytest-django/pull/258 24 | """ 25 | 26 | def test_get_success_url(self, user: User, rf: RequestFactory): 27 | view = UserUpdateView() 28 | request = rf.get("/fake-url/") 29 | request.user = user 30 | 31 | view.request = request 32 | 33 | assert view.get_success_url() == f"/users/{user.username}/" 34 | 35 | def test_get_object(self, user: User, rf: RequestFactory): 36 | view = UserUpdateView() 37 | request = rf.get("/fake-url/") 38 | request.user = user 39 | 40 | view.request = request 41 | 42 | assert view.get_object() == user 43 | 44 | 45 | class TestUserRedirectView: 46 | def test_get_redirect_url(self, user: User, rf: RequestFactory): 47 | view = UserRedirectView() 48 | request = rf.get("/fake-url") 49 | request.user = user 50 | 51 | view.request = request 52 | 53 | assert view.get_redirect_url() == f"/users/{user.username}/" 54 | 55 | 56 | class TestUserDetailView: 57 | def test_authenticated(self, user: User, rf: RequestFactory): 58 | request = rf.get("/fake-url/") 59 | request.user = UserFactory() 60 | 61 | response = user_detail_view(request, username=user.username) 62 | 63 | assert response.status_code == 200 64 | 65 | def test_not_authenticated(self, user: User, rf: RequestFactory): 66 | request = rf.get("/fake-url/") 67 | request.user = AnonymousUser() # type: ignore 68 | 69 | response = user_detail_view(request, username=user.username) 70 | 71 | assert response.status_code == 302 72 | assert response.url == "/accounts/login/?next=/fake-url/" 73 | 74 | def test_case_sensitivity(self, rf: RequestFactory): 75 | request = rf.get("/fake-url/") 76 | request.user = UserFactory(username="UserName") 77 | 78 | with pytest.raises(Http404): 79 | user_detail_view(request, username="username") 80 | -------------------------------------------------------------------------------- /chess_api_project/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from chess_api_project.users.views import ( 4 | user_detail_view, 5 | user_redirect_view, 6 | user_update_view, 7 | ) 8 | 9 | app_name = "users" 10 | urlpatterns = [ 11 | path("~redirect/", view=user_redirect_view, name="redirect"), 12 | path("~update/", view=user_update_view, name="update"), 13 | path("/", view=user_detail_view, name="detail"), 14 | ] 15 | -------------------------------------------------------------------------------- /chess_api_project/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.mixins import LoginRequiredMixin 4 | from django.urls import reverse 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.views.generic import DetailView, RedirectView, UpdateView 7 | 8 | User = get_user_model() 9 | 10 | 11 | class UserDetailView(LoginRequiredMixin, DetailView): 12 | 13 | model = User 14 | slug_field = "username" 15 | slug_url_kwarg = "username" 16 | 17 | 18 | user_detail_view = UserDetailView.as_view() 19 | 20 | 21 | class UserUpdateView(LoginRequiredMixin, UpdateView): 22 | 23 | model = User 24 | fields = ["name"] 25 | 26 | def get_success_url(self): 27 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 28 | 29 | def get_object(self): 30 | return User.objects.get(username=self.request.user.username) 31 | 32 | def form_valid(self, form): 33 | messages.add_message( 34 | self.request, messages.INFO, _("Infos successfully updated") 35 | ) 36 | return super().form_valid(form) 37 | 38 | 39 | user_update_view = UserUpdateView.as_view() 40 | 41 | 42 | class UserRedirectView(LoginRequiredMixin, RedirectView): 43 | 44 | permanent = False 45 | 46 | def get_redirect_url(self): 47 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 48 | 49 | 50 | user_redirect_view = UserRedirectView.as_view() 51 | -------------------------------------------------------------------------------- /chess_api_project/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/chess_api_project/utils/__init__.py -------------------------------------------------------------------------------- /chess_api_project/utils/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def settings_context(_request): 5 | return {"settings": settings} 6 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/config/__init__.py -------------------------------------------------------------------------------- /config/api_router.py: -------------------------------------------------------------------------------- 1 | from api.views import EloViewSet, GameViewSet 2 | from django.conf import settings 3 | from rest_framework.routers import DefaultRouter, SimpleRouter 4 | 5 | from chess_api_project.users.api.views import UserViewSet 6 | 7 | if settings.DEBUG: 8 | router = DefaultRouter() 9 | else: 10 | router = SimpleRouter() 11 | 12 | 13 | router.register("users", UserViewSet) 14 | router.register("game", GameViewSet) 15 | router.register("elo", EloViewSet) 16 | 17 | app_name = "api" 18 | urlpatterns = router.urls 19 | -------------------------------------------------------------------------------- /config/routing.py: -------------------------------------------------------------------------------- 1 | import stream_app.routing 2 | from channels.auth import AuthMiddlewareStack 3 | from channels.routing import ProtocolTypeRouter, URLRouter 4 | 5 | application = ProtocolTypeRouter( 6 | { 7 | "websocket": AuthMiddlewareStack( 8 | URLRouter(stream_app.routing.websocket_urlpatterns) 9 | ), 10 | } 11 | ) 12 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .base import env 3 | 4 | # GENERAL 5 | # ------------------------------------------------------------------------------ 6 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 7 | DEBUG = True 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 9 | SECRET_KEY = env( 10 | "DJANGO_SECRET_KEY", 11 | default="NeitZd8pH2YMdJcDtT2D1S1wBrpTYrdfo1QPjYvWMDQxGmWf9n0dM06TfBkV4wFF", 12 | ) 13 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 14 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] 15 | 16 | # CACHES 17 | # ------------------------------------------------------------------------------ 18 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 19 | CACHES = { 20 | "default": { 21 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 22 | "LOCATION": "", 23 | } 24 | } 25 | 26 | # EMAIL 27 | # ------------------------------------------------------------------------------ 28 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 29 | EMAIL_BACKEND = env( 30 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 31 | ) 32 | 33 | # WhiteNoise 34 | # ------------------------------------------------------------------------------ 35 | # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development 36 | INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405 37 | 38 | 39 | # django-debug-toolbar 40 | # ------------------------------------------------------------------------------ 41 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites 42 | INSTALLED_APPS += ["debug_toolbar"] # noqa F405 43 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware 44 | MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 45 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config 46 | DEBUG_TOOLBAR_CONFIG = { 47 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 48 | "SHOW_TEMPLATE_CONTEXT": True, 49 | } 50 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips 51 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] 52 | 53 | 54 | # django-extensions 55 | # ------------------------------------------------------------------------------ 56 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration 57 | INSTALLED_APPS += ["django_extensions"] # noqa F405 58 | -------------------------------------------------------------------------------- /config/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .base import env 3 | 4 | # GENERAL 5 | # ------------------------------------------------------------------------------ 6 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 7 | SECRET_KEY = env("DJANGO_SECRET_KEY") 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 9 | ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["fullstackdjango.com"]) 10 | 11 | # DATABASES 12 | # ------------------------------------------------------------------------------ 13 | DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 14 | DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 15 | DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 16 | 17 | # CACHES 18 | # ------------------------------------------------------------------------------ 19 | CACHES = { 20 | "default": { 21 | "BACKEND": "django_redis.cache.RedisCache", 22 | "LOCATION": env("REDIS_URL"), 23 | "OPTIONS": { 24 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 25 | # Mimicing memcache behavior. 26 | # http://jazzband.github.io/django-redis/latest/#_memcached_exceptions_behavior 27 | "IGNORE_EXCEPTIONS": True, 28 | }, 29 | } 30 | } 31 | 32 | # SECURITY 33 | # ------------------------------------------------------------------------------ 34 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header 35 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 36 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect 37 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) 38 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure 39 | SESSION_COOKIE_SECURE = True 40 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure 41 | CSRF_COOKIE_SECURE = True 42 | # https://docs.djangoproject.com/en/dev/topics/security/#ssl-https 43 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds 44 | # TODO: set this to 60 seconds first and then to 518400 once you prove the former works 45 | SECURE_HSTS_SECONDS = 60 46 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains 47 | SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( 48 | "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True 49 | ) 50 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload 51 | SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) 52 | # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff 53 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool( 54 | "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True 55 | ) 56 | 57 | # STATIC 58 | # ------------------------ 59 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 60 | # MEDIA 61 | # ------------------------------------------------------------------------------ 62 | 63 | # TEMPLATES 64 | # ------------------------------------------------------------------------------ 65 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 66 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 67 | ( 68 | "django.template.loaders.cached.Loader", 69 | [ 70 | "django.template.loaders.filesystem.Loader", 71 | "django.template.loaders.app_directories.Loader", 72 | ], 73 | ) 74 | ] 75 | 76 | # EMAIL 77 | # ------------------------------------------------------------------------------ 78 | # https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email 79 | DEFAULT_FROM_EMAIL = env( 80 | "DJANGO_DEFAULT_FROM_EMAIL", 81 | default="Django Chess API ", 82 | ) 83 | # https://docs.djangoproject.com/en/dev/ref/settings/#server-email 84 | SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) 85 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix 86 | EMAIL_SUBJECT_PREFIX = env("DJANGO_EMAIL_SUBJECT_PREFIX", default="[Django Chess API]") 87 | 88 | # ADMIN 89 | # ------------------------------------------------------------------------------ 90 | # Django Admin URL regex. 91 | ADMIN_URL = env("DJANGO_ADMIN_URL") 92 | 93 | # Anymail 94 | # ------------------------------------------------------------------------------ 95 | # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail 96 | INSTALLED_APPS += ["anymail"] # noqa F405 97 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 98 | # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference 99 | # https://anymail.readthedocs.io/en/stable/esps 100 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 101 | ANYMAIL = {} 102 | 103 | 104 | # LOGGING 105 | # ------------------------------------------------------------------------------ 106 | # https://docs.djangoproject.com/en/dev/ref/settings/#logging 107 | # See https://docs.djangoproject.com/en/dev/topics/logging for 108 | # more details on how to customize your logging configuration. 109 | # A sample logging configuration. The only tangible logging 110 | # performed by this configuration is to send an email to 111 | # the site admins on every HTTP 500 error when DEBUG=False. 112 | LOGGING = { 113 | "version": 1, 114 | "disable_existing_loggers": False, 115 | "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, 116 | "formatters": { 117 | "verbose": { 118 | "format": "%(levelname)s %(asctime)s %(module)s " 119 | "%(process)d %(thread)d %(message)s" 120 | } 121 | }, 122 | "handlers": { 123 | "mail_admins": { 124 | "level": "ERROR", 125 | "filters": ["require_debug_false"], 126 | "class": "django.utils.log.AdminEmailHandler", 127 | }, 128 | "console": { 129 | "level": "DEBUG", 130 | "class": "logging.StreamHandler", 131 | "formatter": "verbose", 132 | }, 133 | }, 134 | "root": {"level": "INFO", "handlers": ["console"]}, 135 | "loggers": { 136 | "django.request": { 137 | "handlers": ["mail_admins"], 138 | "level": "ERROR", 139 | "propagate": True, 140 | }, 141 | "django.security.DisallowedHost": { 142 | "level": "ERROR", 143 | "handlers": ["console", "mail_admins"], 144 | "propagate": True, 145 | }, 146 | }, 147 | } 148 | 149 | # Your stuff... 150 | # ------------------------------------------------------------------------------ 151 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | 5 | from .base import * # noqa 6 | from .base import env 7 | 8 | # GENERAL 9 | # ------------------------------------------------------------------------------ 10 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 11 | SECRET_KEY = env( 12 | "DJANGO_SECRET_KEY", 13 | default="sVoKpv0zoA21Ze4LRDGhIz8O2lkCnC6UEEqjzK7trefNZ27fo0VBSZKuIBGETQBe", 14 | ) 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 16 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 17 | 18 | # CACHES 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 21 | CACHES = { 22 | "default": { 23 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 24 | "LOCATION": "", 25 | } 26 | } 27 | 28 | # PASSWORDS 29 | # ------------------------------------------------------------------------------ 30 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 31 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 32 | 33 | # TEMPLATES 34 | # ------------------------------------------------------------------------------ 35 | TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 36 | ( 37 | "django.template.loaders.cached.Loader", 38 | [ 39 | "django.template.loaders.filesystem.Loader", 40 | "django.template.loaders.app_directories.Loader", 41 | ], 42 | ) 43 | ] 44 | 45 | # EMAIL 46 | # ------------------------------------------------------------------------------ 47 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 48 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 49 | 50 | # Your stuff... 51 | # ------------------------------------------------------------------------------ 52 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from api.views import CustomTokenObtainPairView 2 | 3 | from django.conf import settings 4 | from django.conf.urls.static import static 5 | from django.contrib import admin 6 | from django.urls import include, path 7 | from django.views import defaults as default_views 8 | from django.views.generic import TemplateView 9 | 10 | from rest_framework.authtoken.views import obtain_auth_token 11 | from rest_framework_simplejwt.views import TokenRefreshView 12 | 13 | from drf_yasg.views import get_schema_view 14 | from drf_yasg import openapi 15 | 16 | 17 | schema_view = get_schema_view( 18 | openapi.Info( 19 | title="Capablanca API", 20 | description="Multiplayer Chess API", 21 | default_version="0.2.0", 22 | urlconf="config.api_router", 23 | ), 24 | ) 25 | 26 | urlpatterns = [ 27 | path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), 28 | path( 29 | "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 30 | ), 31 | # Django Admin, use {% url 'admin:index' %} 32 | path(settings.ADMIN_URL, admin.site.urls), 33 | # User management 34 | path("users/", include("chess_api_project.users.urls", namespace="users")), 35 | path("accounts/", include("allauth.urls")), 36 | # OpenAPI 37 | path( 38 | "docs/", schema_view.with_ui("swagger", cache_timeout=0), name="openapi-schema", 39 | ), 40 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 41 | 42 | # API URLS 43 | urlpatterns += [ 44 | # API base url 45 | path("api/", include("config.api_router")), 46 | # Simple JWT 47 | path("api/token/", CustomTokenObtainPairView.as_view(), name="token_obtain_pair"), 48 | path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), 49 | # DRF auth token 50 | path("auth-token/", obtain_auth_token), 51 | # DRF auth 52 | path("api-auth/", include("rest_framework.urls")), 53 | ] 54 | 55 | if settings.DEBUG: 56 | # This allows the error pages to be debugged during development, just visit 57 | # these url in browser to see how these error pages look like. 58 | urlpatterns += [ 59 | path( 60 | "400/", 61 | default_views.bad_request, 62 | kwargs={"exception": Exception("Bad Request!")}, 63 | ), 64 | path( 65 | "403/", 66 | default_views.permission_denied, 67 | kwargs={"exception": Exception("Permission Denied")}, 68 | ), 69 | path( 70 | "404/", 71 | default_views.page_not_found, 72 | kwargs={"exception": Exception("Page not Found")}, 73 | ), 74 | path("500/", default_views.server_error), 75 | ] 76 | if "debug_toolbar" in settings.INSTALLED_APPS: 77 | import debug_toolbar 78 | 79 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns 80 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for Django Chess API project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | import sys 18 | from pathlib import Path 19 | 20 | from django.core.wsgi import get_wsgi_application 21 | 22 | # This allows easy placement of apps within the interior 23 | # chess_api_project directory. 24 | ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent 25 | sys.path.append(str(ROOT_DIR / "chess_api_project")) 26 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 27 | # if running multiple sites in the same mod_wsgi process. To fix this, use 28 | # mod_wsgi daemon mode with each site in its own daemon process, or use 29 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" 30 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") 31 | 32 | # This application object is used by any WSGI server configured to use this 33 | # file. This includes Django's development server, if the WSGI_APPLICATION 34 | # setting points here. 35 | application = get_wsgi_application() 36 | # Apply WSGI middleware here. 37 | # from helloworld.wsgi import HelloWorldApplication 38 | # application = HelloWorldApplication(application) 39 | -------------------------------------------------------------------------------- /docs/#results.org#: -------------------------------------------------------------------------------- 1 | * [[https://chess.stackexchange.com/questions/12403/what-are-the-possible-results-of-a-game][Results]] 2 | 3 | ** Scheduled 4 | ** Postponed 5 | ** Finished without any moves played 6 | ** In progress 7 | ** Adjourned 8 | ** Finished according to the Basic Rules of Play 9 | ** Finished by the clock 10 | ** Draw 11 | ** Finished because of a breach of rules of one player 12 | ** Finished because both players persistently refuse to comply with the laws of chess 13 | ** To be decided 14 | ** Abandoned 15 | ** Unknown 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # Included so that Django's startproject comment runs against the docs directory 2 | -------------------------------------------------------------------------------- /docs/api_coverage/api___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for api/__init__.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |
55 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/api_coverage/api_admin_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for api/admin.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1# Register your models here. 

55 |
56 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/api_coverage/api_apps_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for api/apps.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1from django.apps import AppConfig 

55 |

2 

56 |

3 

57 |

4class ApiConfig(AppConfig): 

58 |

5 name = "api" 

59 |
60 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/api_coverage/api_constants_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for api/constants.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1K_FACTOR = 32 

55 |
56 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/api_coverage/api_urls_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for api/urls.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1# posts/urls.py 

55 |

2from django.urls import include, path 

56 |

3from rest_framework.routers import DefaultRouter 

57 |

4 

58 |

5from .views import EloViewSet, GameViewSet 

59 |

6 

60 |

7router = DefaultRouter() 

61 |

8router.register(r"game", GameViewSet) 

62 |

9router.register(r"elo", EloViewSet) 

63 |

10 

64 |

11 

65 |

12urlpatterns = [ 

66 |

13 path("", include(router.urls)), 

67 |

14] 

68 |
69 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /docs/api_coverage/jquery.ba-throttle-debounce.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery throttle / debounce - v1.1 - 3/7/2010 3 | * http://benalman.com/projects/jquery-throttle-debounce-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); 10 | -------------------------------------------------------------------------------- /docs/api_coverage/jquery.hotkeys.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Hotkeys Plugin 3 | * Copyright 2010, John Resig 4 | * Dual licensed under the MIT or GPL Version 2 licenses. 5 | * 6 | * Based upon the plugin by Tzury Bar Yochay: 7 | * http://github.com/tzuryby/hotkeys 8 | * 9 | * Original idea by: 10 | * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ 11 | */ 12 | 13 | (function(jQuery){ 14 | 15 | jQuery.hotkeys = { 16 | version: "0.8", 17 | 18 | specialKeys: { 19 | 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", 20 | 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 21 | 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 22 | 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 23 | 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 24 | 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 25 | 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" 26 | }, 27 | 28 | shiftNums: { 29 | "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", 30 | "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", 31 | ".": ">", "/": "?", "\\": "|" 32 | } 33 | }; 34 | 35 | function keyHandler( handleObj ) { 36 | // Only care when a possible input has been specified 37 | if ( typeof handleObj.data !== "string" ) { 38 | return; 39 | } 40 | 41 | var origHandler = handleObj.handler, 42 | keys = handleObj.data.toLowerCase().split(" "); 43 | 44 | handleObj.handler = function( event ) { 45 | // Don't fire in text-accepting inputs that we didn't directly bind to 46 | if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || 47 | event.target.type === "text") ) { 48 | return; 49 | } 50 | 51 | // Keypress represents characters, not special keys 52 | var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], 53 | character = String.fromCharCode( event.which ).toLowerCase(), 54 | key, modif = "", possible = {}; 55 | 56 | // check combinations (alt|ctrl|shift+anything) 57 | if ( event.altKey && special !== "alt" ) { 58 | modif += "alt+"; 59 | } 60 | 61 | if ( event.ctrlKey && special !== "ctrl" ) { 62 | modif += "ctrl+"; 63 | } 64 | 65 | // TODO: Need to make sure this works consistently across platforms 66 | if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { 67 | modif += "meta+"; 68 | } 69 | 70 | if ( event.shiftKey && special !== "shift" ) { 71 | modif += "shift+"; 72 | } 73 | 74 | if ( special ) { 75 | possible[ modif + special ] = true; 76 | 77 | } else { 78 | possible[ modif + character ] = true; 79 | possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; 80 | 81 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" 82 | if ( modif === "shift+" ) { 83 | possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; 84 | } 85 | } 86 | 87 | for ( var i = 0, l = keys.length; i < l; i++ ) { 88 | if ( possible[ keys[i] ] ) { 89 | return origHandler.apply( this, arguments ); 90 | } 91 | } 92 | }; 93 | } 94 | 95 | jQuery.each([ "keydown", "keyup", "keypress" ], function() { 96 | jQuery.event.special[ this ] = { add: keyHandler }; 97 | }); 98 | 99 | })( jQuery ); 100 | -------------------------------------------------------------------------------- /docs/api_coverage/jquery.isonscreen.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2010 2 | * @author Laurence Wheway 3 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 4 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 5 | * 6 | * @version 1.2.0 7 | */ 8 | (function($) { 9 | jQuery.extend({ 10 | isOnScreen: function(box, container) { 11 | //ensure numbers come in as intgers (not strings) and remove 'px' is it's there 12 | for(var i in box){box[i] = parseFloat(box[i])}; 13 | for(var i in container){container[i] = parseFloat(container[i])}; 14 | 15 | if(!container){ 16 | container = { 17 | left: $(window).scrollLeft(), 18 | top: $(window).scrollTop(), 19 | width: $(window).width(), 20 | height: $(window).height() 21 | } 22 | } 23 | 24 | if( box.left+box.width-container.left > 0 && 25 | box.left < container.width+container.left && 26 | box.top+box.height-container.top > 0 && 27 | box.top < container.height+container.top 28 | ) return true; 29 | return false; 30 | } 31 | }) 32 | 33 | 34 | jQuery.fn.isOnScreen = function (container) { 35 | for(var i in container){container[i] = parseFloat(container[i])}; 36 | 37 | if(!container){ 38 | container = { 39 | left: $(window).scrollLeft(), 40 | top: $(window).scrollTop(), 41 | width: $(window).width(), 42 | height: $(window).height() 43 | } 44 | } 45 | 46 | if( $(this).offset().left+$(this).width()-container.left > 0 && 47 | $(this).offset().left < container.width+container.left && 48 | $(this).offset().top+$(this).height()-container.top > 0 && 49 | $(this).offset().top < container.height+container.top 50 | ) return true; 51 | return false; 52 | } 53 | })(jQuery); 54 | -------------------------------------------------------------------------------- /docs/api_coverage/keybd_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/docs/api_coverage/keybd_closed.png -------------------------------------------------------------------------------- /docs/api_coverage/keybd_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/docs/api_coverage/keybd_open.png -------------------------------------------------------------------------------- /docs/api_coverage/status.json: -------------------------------------------------------------------------------- 1 | {"format":2,"version":"5.1","globals":"3729063479803456f8cdc27da4ccc9fd","files":{"api___init___py":{"hash":"af9d0d0de0ea3b71d198bc63f1499a7d","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"api___init___py.html","relative_filename":"api/__init__.py"}},"api_admin_py":{"hash":"3964635549ef6016964175c9db70b348","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"api_admin_py.html","relative_filename":"api/admin.py"}},"api_apps_py":{"hash":"c821bdebef7d32ffdf1f25f69837ef74","index":{"nums":[1,3,0,0,0,0,0],"html_filename":"api_apps_py.html","relative_filename":"api/apps.py"}},"api_constants_py":{"hash":"baadc6b7606d9f07be3417a354847df8","index":{"nums":[1,1,0,0,0,0,0],"html_filename":"api_constants_py.html","relative_filename":"api/constants.py"}},"api_models_py":{"hash":"a48396a43f9169991bffa0f465d636a0","index":{"nums":[1,138,0,6,0,0,0],"html_filename":"api_models_py.html","relative_filename":"api/models.py"}},"api_permissions_py":{"hash":"e63abecc72487d0f42a638fc7189495a","index":{"nums":[1,22,0,1,0,0,0],"html_filename":"api_permissions_py.html","relative_filename":"api/permissions.py"}},"api_serializers_py":{"hash":"7f06c4a2b888bcbee3cc63439bb4e0d7","index":{"nums":[1,44,0,0,0,0,0],"html_filename":"api_serializers_py.html","relative_filename":"api/serializers.py"}},"api_services_py":{"hash":"4789efa84e73a7ced8d25bc5aec19c73","index":{"nums":[1,113,0,1,0,0,0],"html_filename":"api_services_py.html","relative_filename":"api/services.py"}},"api_views_py":{"hash":"1dc74532c431108f97db8e56e11a811e","index":{"nums":[1,50,0,0,0,0,0],"html_filename":"api_views_py.html","relative_filename":"api/views.py"}},"stream_app___init___py":{"hash":"af9d0d0de0ea3b71d198bc63f1499a7d","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"stream_app___init___py.html","relative_filename":"stream_app/__init__.py"}},"stream_app_admin_py":{"hash":"3964635549ef6016964175c9db70b348","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"stream_app_admin_py.html","relative_filename":"stream_app/admin.py"}},"stream_app_apps_py":{"hash":"fe9eeb10169c9d06e90ea215f0ca10c4","index":{"nums":[1,3,0,0,0,0,0],"html_filename":"stream_app_apps_py.html","relative_filename":"stream_app/apps.py"}},"stream_app_consumers_py":{"hash":"6ab43602e551c3028f97d89296480f00","index":{"nums":[1,20,0,0,0,0,0],"html_filename":"stream_app_consumers_py.html","relative_filename":"stream_app/consumers.py"}},"stream_app_models_py":{"hash":"8716d289d5120793bf2752a20189df39","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"stream_app_models_py.html","relative_filename":"stream_app/models.py"}},"stream_app_routing_py":{"hash":"94450d32d897b440b9babb2d8018cd60","index":{"nums":[1,3,0,0,0,0,0],"html_filename":"stream_app_routing_py.html","relative_filename":"stream_app/routing.py"}},"stream_app_services_py":{"hash":"9028cf1a00c87346bbbeb658b7621046","index":{"nums":[1,7,0,0,0,0,0],"html_filename":"stream_app_services_py.html","relative_filename":"stream_app/services.py"}},"stream_app_views_py":{"hash":"8f59a7e9210db62370cd600c1c607645","index":{"nums":[1,0,0,0,0,0,0],"html_filename":"stream_app_views_py.html","relative_filename":"stream_app/views.py"}}}} -------------------------------------------------------------------------------- /docs/api_coverage/stream_app___init___py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/__init__.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |
55 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/api_coverage/stream_app_admin_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/admin.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1# Register your models here. 

55 |
56 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/api_coverage/stream_app_apps_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/apps.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1from django.apps import AppConfig 

55 |

2 

56 |

3 

57 |

4class StreamAppConfig(AppConfig): 

58 |

5 name = "stream_app" 

59 |
60 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/api_coverage/stream_app_models_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/models.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1# Create your models here. 

55 |
56 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/api_coverage/stream_app_routing_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/routing.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1from django.urls import path 

55 |

2 

56 |

3from . import consumers 

57 |

4 

58 |

5websocket_urlpatterns = [ 

59 |

6 path(r"ws/game/<uuid>/", consumers.GameConsumer), 

60 |

7] 

61 |
62 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/api_coverage/stream_app_services_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/services.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |

1from channels.db import database_sync_to_async 

55 |

2from api.models import Game 

56 |

3from api.serializers import GameSerializer 

57 |

4 

58 |

5 

59 |

6@database_sync_to_async 

60 |

7def get_serialized_game(uuid): 

61 |

8 game = Game.objects.get(uuid=uuid) 

62 |

9 return GameSerializer(game).data 

63 |
64 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/api_coverage/stream_app_views_py.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Coverage for stream_app/views.py: 100% 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 31 |
32 | Hide keyboard shortcuts 33 |

Hot-keys on this page

34 |
35 |

36 | r 37 | m 38 | x 39 | p   toggle line displays 40 |

41 |

42 | j 43 | k   next/prev highlighted chunk 44 |

45 |

46 | 0   (zero) top of page 47 |

48 |

49 | 1   (one) first highlighted chunk 50 |

51 |
52 |
53 |
54 |
55 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/chess_api.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/docs/chess_api.pdf -------------------------------------------------------------------------------- /docs/client_implementation_guide.org: -------------------------------------------------------------------------------- 1 | * [[https://www.codementor.io/@obabichev/react-token-auth-12os8txqo1][Token Auth]] 2 | 1. Tokens should be stored in local storage 3 | 2. Tokens should be restored on page reload 4 | 3. Access token should be passed in the network requests 5 | 4. After expiration access token should be updated by refresh token if the last one is presented 6 | 5. React components should have access to the auth information to render appropriate UI 7 | 6. The solution should be made with pure React (without Redux, thunk, etc..) 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | 14 | # import django 15 | # sys.path.insert(0, os.path.abspath('..')) 16 | # os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 17 | # django.setup() 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "Django Chess API" 23 | copyright = """2020, Mikel Losada""" 24 | author = "Mikel Losada" 25 | 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 41 | 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = "alabaster" 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ["_static"] 54 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django Chess API documentation master file, created by 2 | sphinx-quickstart. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django Chess API's documentation! 7 | ====================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | pycharm/configuration 14 | 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/results.org: -------------------------------------------------------------------------------- 1 | 2 | * [[https://chess.stackexchange.com/questions/12403/what-are-the-possible-results-of-a-game][Results]] 3 | 4 | ** Scheduled 5 | ** Postponed 6 | ** Finished without any moves played 7 | ** In progress 8 | ** Adjourned 9 | ** Finished according to the Basic Rules of Play 10 | ** Finished by the clock 11 | ** Draw 12 | ** Finished because of a breach of rules of one player 13 | ** Finished because both players persistently refuse to comply with the laws of chess 14 | ** To be decided 15 | ** Abandoned 16 | ** Unknown 17 | -------------------------------------------------------------------------------- /docs/time_control.org: -------------------------------------------------------------------------------- 1 | * FIDE time control 2 | There is a single time control for all major FIDE events: 90 minutes for the first 40 moves followed by 30 minutes for the rest of the game with an addition of 30 seconds per move starting from move one. 3 | -------------------------------------------------------------------------------- /locale/README.rst: -------------------------------------------------------------------------------- 1 | Translations 2 | ============ 3 | 4 | Translations will be placed in this folder when running:: 5 | 6 | python manage.py makemessages 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 8 | 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError: 12 | # The above import may fail for some other reason. Ensure that the 13 | # issue is really that Django is missing to avoid masking other 14 | # exceptions on Python 2. 15 | try: 16 | import django # noqa 17 | except ImportError: 18 | raise ImportError( 19 | "Couldn't import Django. Are you sure it's installed and " 20 | "available on your PYTHONPATH environment variable? Did you " 21 | "forget to activate a virtual environment?" 22 | ) 23 | 24 | raise 25 | 26 | # This allows easy placement of apps within the interior 27 | # chess_api_project directory. 28 | current_path = Path(__file__).parent.resolve() 29 | sys.path.append(str(current_path / "chess_api_project")) 30 | 31 | execute_from_command_line(sys.argv) 32 | -------------------------------------------------------------------------------- /openapi-schema.yml: -------------------------------------------------------------------------------- 1 | info: 2 | description: '' 3 | title: '' 4 | version: '' 5 | openapi: 3.0.0 6 | paths: 7 | /auth-token/: 8 | post: 9 | operationId: auth-token_create 10 | tags: 11 | - auth-token 12 | servers: 13 | - url: '' 14 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ds=config.settings.test --reuse-db 3 | python_files = tests.py test_*.py 4 | -------------------------------------------------------------------------------- /requirements/#local.txt#: -------------------------------------------------------------------------------- 1 | -r ./base.txt 2 | 3 | Werkzeug==1.0.1 # https://github.com/pallets/werkzeug 4 | ipdb==0.13.2 # https://github.com/gotcha/ipdb 5 | Sphinx==3.0.4 # https://github.com/sphinx-doc/sphinx 6 | psycopg2-binary==2.8.5 # https://github.com/psycopg/psycopg2 7 | 8 | # Testing 9 | # ------------------------------------------------------------------------------ 10 | mypy==0.770 # https://github.com/python/mypy 11 | django-stubs==1.5.0 # https://github.com/typeddjango/django-stubs 12 | pytest==5.4.3 # https://github.com/pytest-dev/pytest 13 | pytest-sugar==0.9.3 # https://github.com/Frozenball/pytest-sugar 14 | 15 | # Code quality 16 | # ------------------------------------------------------------------------------ 17 | flake8==3.8.2 # https://github.com/PyCQA/flake8 18 | flake8-isort==3.0.0 # https://github.com/gforcada/flake8-isort 19 | coverage==5.1 # https://github.com/nedbat/coveragepy 20 | black==19.10b0 # https://github.com/ambv/black 21 | pylint-django==2.0.15 # https://github.com/PyCQA/pylint-django 22 | pre-commit==2.4.0 # https://github.com/pre-commit/pre-commit 23 | 24 | # Django 25 | # ------------------------------------------------------------------------------ 26 | factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy 27 | 28 | django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar 29 | django-extensions==2.2.9 # https://github.com/django-extensions/django-extensions 30 | django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin 31 | pytest-django==3.9.0 # https://github.com/pytest-dev/pytest-django 32 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | pytz==2020.1 # https://github.com/stub42/pytz 2 | python-slugify==4.0.0 # https://github.com/un33k/python-slugify 3 | Pillow==7.1.2 # https://github.com/python-pillow/Pillow 4 | argon2-cffi==19.2.0 # https://github.com/hynek/argon2_cffi 5 | whitenoise==5.0.1 # https://github.com/evansd/whitenoise 6 | redis==3.5.0 # https://github.com/andymccurdy/redis-py 7 | hiredis==1.0.1 # https://github.com/redis/hiredis-py 8 | python-chess==0.31.2 9 | coveralls==2.1.1 10 | 11 | # Django 12 | # ------------------------------------------------------------------------------ 13 | django==3.0.7 # pyup: < 3.1 # https://www.djangoproject.com/ 14 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 15 | django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils 16 | django-allauth==0.41.0 # https://github.com/pennersr/django-allauth 17 | django-crispy-forms==1.9.0 # https://github.com/django-crispy-forms/django-crispy-forms 18 | django-redis==4.11.0 # https://github.com/jazzband/django-redis 19 | django-annoying==0.10.6 20 | # Django REST Framework 21 | djangorestframework==3.11.0 # https://github.com/encode/django-rest-framework 22 | djangorestframework-simplejwt==4.4.0 23 | django-cors-headers==3.4.0 24 | drf-yasg==1.17.1 25 | # Django Channels 26 | channels-redis==3.0.0 27 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r ./base.txt 2 | 3 | Werkzeug==1.0.1 # https://github.com/pallets/werkzeug 4 | ipdb==0.13.2 # https://github.com/gotcha/ipdb 5 | Sphinx==3.0.4 # https://github.com/sphinx-doc/sphinx 6 | psycopg2-binary==2.8.5 # https://github.com/psycopg/psycopg2 7 | 8 | # Testing 9 | # ------------------------------------------------------------------------------ 10 | mypy==0.770 # https://github.com/python/mypy 11 | django-stubs==1.5.0 # https://github.com/typeddjango/django-stubs 12 | pytest==5.4.3 # https://github.com/pytest-dev/pytest 13 | pytest-sugar==0.9.3 # https://github.com/Frozenball/pytest-sugar 14 | pytest-cov==2.10.0 15 | pytest-asyncio==0.14.0 16 | 17 | # Code quality 18 | # ------------------------------------------------------------------------------ 19 | flake8==3.8.2 # https://github.com/PyCQA/flake8 20 | flake8-isort==3.0.0 # https://github.com/gforcada/flake8-isort 21 | coverage==5.1 # https://github.com/nedbat/coveragepy 22 | black==19.10b0 # https://github.com/ambv/black 23 | pylint-django==2.0.15 # https://github.com/PyCQA/pylint-django 24 | pre-commit==2.4.0 # https://github.com/pre-commit/pre-commit 25 | 26 | # Django 27 | # ------------------------------------------------------------------------------ 28 | factory-boy==2.12.0 # https://github.com/FactoryBoy/factory_boy 29 | 30 | django-debug-toolbar==2.2 # https://github.com/jazzband/django-debug-toolbar 31 | django-extensions==2.2.9 # https://github.com/django-extensions/django-extensions 32 | django-coverage-plugin==1.8.0 # https://github.com/nedbat/django_coverage_plugin 33 | pytest-django==3.9.0 # https://github.com/pytest-dev/pytest-django 34 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | # PRECAUTION: avoid production dependencies that aren't in development 2 | 3 | -r ./base.txt 4 | 5 | gunicorn==20.0.4 # https://github.com/benoitc/gunicorn 6 | psycopg2==2.8.5 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 7 | 8 | # Django 9 | # ------------------------------------------------------------------------------ 10 | django-anymail==7.1.0 # https://github.com/anymail/django-anymail 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules 4 | 5 | [pycodestyle] 6 | max-line-length = 120 7 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules 8 | 9 | [mypy] 10 | python_version = 3.8 11 | check_untyped_defs = True 12 | ignore_missing_imports = True 13 | warn_unused_ignores = True 14 | warn_redundant_casts = True 15 | warn_unused_configs = True 16 | plugins = mypy_django_plugin.main 17 | 18 | [mypy.plugins.django-stubs] 19 | django_settings_module = config.settings.test 20 | 21 | [mypy-*.migrations.*] 22 | # Django migrations should not produce any errors: 23 | ignore_errors = True 24 | 25 | [coverage:run] 26 | include = chess_api_project/* 27 | omit = *migrations*, *tests* 28 | plugins = 29 | django_coverage_plugin 30 | -------------------------------------------------------------------------------- /stream_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/stream_app/__init__.py -------------------------------------------------------------------------------- /stream_app/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /stream_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class StreamAppConfig(AppConfig): 5 | name = "stream_app" 6 | -------------------------------------------------------------------------------- /stream_app/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | from channels.generic.websocket import AsyncWebsocketConsumer 5 | 6 | from stream_app.services import get_serialized_game 7 | 8 | 9 | class GameConsumer(AsyncWebsocketConsumer): 10 | async def connect(self): 11 | self.uuid = self.scope["url_route"]["kwargs"]["uuid"] 12 | self.game_group_name = f"game_{self.uuid}" 13 | 14 | # Join room group 15 | await self.channel_layer.group_add(self.game_group_name, self.channel_name) 16 | 17 | await self.accept() 18 | 19 | async def receive(self, text_data): 20 | data_json = json.loads(text_data) 21 | 22 | if "update" in data_json: 23 | game = await get_serialized_game(data_json.get("uuid")) 24 | 25 | # Update both players' game data 26 | await self.channel_layer.group_send( 27 | self.game_group_name, {"type": "game_data", "game": game,} 28 | ) 29 | 30 | async def game_data(self, data): 31 | game = data["game"] 32 | 33 | # Send game over WebSocket 34 | await self.send(text_data=json.dumps(game)) 35 | 36 | async def disconnect(self, *args, **kwargs): 37 | await self.channel_layer.group_discard(self.game_group_name, self.channel_name) 38 | -------------------------------------------------------------------------------- /stream_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/stream_app/migrations/__init__.py -------------------------------------------------------------------------------- /stream_app/models.py: -------------------------------------------------------------------------------- 1 | # Create your models here. 2 | -------------------------------------------------------------------------------- /stream_app/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import consumers 4 | 5 | websocket_urlpatterns = [ 6 | path(r"ws/game//", consumers.GameConsumer), 7 | ] 8 | -------------------------------------------------------------------------------- /stream_app/services.py: -------------------------------------------------------------------------------- 1 | from channels.db import database_sync_to_async 2 | from api.models import Game 3 | from api.serializers import GameSerializer 4 | 5 | 6 | @database_sync_to_async 7 | def get_serialized_game(uuid): 8 | game = Game.objects.get(uuid=uuid) 9 | return GameSerializer(game).data 10 | -------------------------------------------------------------------------------- /stream_app/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | GAME_UUID = "fe73b193-4f0d-4b0a-b964-b2bce3540413" 2 | -------------------------------------------------------------------------------- /stream_app/tests/test_consumers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | 4 | from uuid import uuid4 5 | from unittest import mock 6 | 7 | from channels.testing import WebsocketCommunicator 8 | from stream_app.consumers import GameConsumer 9 | 10 | 11 | from api.models import Game 12 | 13 | from stream_app.tests.fixtures import GAME_UUID 14 | 15 | 16 | @pytest.mark.asyncio 17 | @pytest.fixture 18 | def url_route(): 19 | return { 20 | "kwargs": {"uuid": str(uuid4()),}, 21 | } 22 | 23 | 24 | @pytest.mark.asyncio 25 | @pytest.fixture 26 | def game_communicator(url_route): 27 | game_uuid = url_route["kwargs"]["uuid"] 28 | communicator = WebsocketCommunicator(GameConsumer, f"/ws/game/{game_uuid}/") 29 | communicator.scope["url_route"] = url_route 30 | 31 | return communicator 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_connect(game_communicator): 36 | connected, _ = await game_communicator.connect() 37 | 38 | assert connected 39 | 40 | await game_communicator.disconnect() 41 | 42 | 43 | @pytest.mark.asyncio 44 | @pytest.mark.django_db 45 | async def test_receive(game_communicator): 46 | connected, _ = await game_communicator.connect() 47 | 48 | Game.objects.get = mock.MagicMock(return_value=Game(uuid=GAME_UUID)) 49 | 50 | await game_communicator.send_to(json.dumps({"update": "foo"})) 51 | received_message = await game_communicator.receive_json_from() 52 | 53 | assert received_message["uuid"] == GAME_UUID 54 | 55 | await game_communicator.disconnect() 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_routing(): 60 | from stream_app import routing 61 | 62 | assert routing.websocket_urlpatterns is not None 63 | -------------------------------------------------------------------------------- /stream_app/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkShoft/capablanca-api/28b81b164dee79d13299d7569c5382e1af4e2c91/stream_app/views.py -------------------------------------------------------------------------------- /utility/install_os_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORK_DIR="$(dirname "$0")" 4 | DISTRO_NAME=$(lsb_release -sc) 5 | OS_REQUIREMENTS_FILENAME="requirements-$DISTRO_NAME.apt" 6 | 7 | cd $WORK_DIR 8 | 9 | # Check if a requirements file exist for the current distribution. 10 | if [ ! -r "$OS_REQUIREMENTS_FILENAME" ]; then 11 | cat <<-EOF >&2 12 | There is no requirements file for your distribution. 13 | You can see one of the files listed below to help search the equivalent package in your system: 14 | $(find ./ -name "requirements-*.apt" -printf " - %f\n") 15 | EOF 16 | exit 1; 17 | fi 18 | 19 | # Handle call with wrong command 20 | function wrong_command() 21 | { 22 | echo "${0##*/} - unknown command: '${1}'" >&2 23 | usage_message 24 | } 25 | 26 | # Print help / script usage 27 | function usage_message() 28 | { 29 | cat <<-EOF 30 | Usage: $WORK_DIR/${0##*/} 31 | Available commands are: 32 | list Print a list of all packages defined on ${OS_REQUIREMENTS_FILENAME} file 33 | help Print this help 34 | 35 | Commands that require superuser permission: 36 | install Install packages defined on ${OS_REQUIREMENTS_FILENAME} file. Note: This 37 | does not upgrade the packages already installed for new versions, even if 38 | new version is available in the repository. 39 | upgrade Same that install, but upgrade the already installed packages, if new 40 | version is available. 41 | EOF 42 | } 43 | 44 | # Read the requirements.apt file, and remove comments and blank lines 45 | function list_packages(){ 46 | grep -v "#" "${OS_REQUIREMENTS_FILENAME}" | grep -v "^$"; 47 | } 48 | 49 | function install_packages() 50 | { 51 | list_packages | xargs apt-get --no-upgrade install -y; 52 | } 53 | 54 | function upgrade_packages() 55 | { 56 | list_packages | xargs apt-get install -y; 57 | } 58 | 59 | function install_or_upgrade() 60 | { 61 | P=${1} 62 | PARAN=${P:-"install"} 63 | 64 | if [[ $EUID -ne 0 ]]; then 65 | cat <<-EOF >&2 66 | You must run this script with root privilege 67 | Please do: 68 | sudo $WORK_DIR/${0##*/} $PARAN 69 | EOF 70 | exit 1 71 | else 72 | 73 | apt-get update 74 | 75 | # Install the basic compilation dependencies and other required libraries of this project 76 | if [ "$PARAN" == "install" ]; then 77 | install_packages; 78 | else 79 | upgrade_packages; 80 | fi 81 | 82 | # cleaning downloaded packages from apt-get cache 83 | apt-get clean 84 | 85 | exit 0 86 | fi 87 | } 88 | 89 | # Handle command argument 90 | case "$1" in 91 | install) install_or_upgrade;; 92 | upgrade) install_or_upgrade "upgrade";; 93 | list) list_packages;; 94 | help|"") usage_message;; 95 | *) wrong_command "$1";; 96 | esac 97 | -------------------------------------------------------------------------------- /utility/install_python_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORK_DIR="$(dirname "$0")" 4 | PROJECT_DIR="$(dirname "$WORK_DIR")" 5 | 6 | pip --version >/dev/null 2>&1 || { 7 | echo >&2 -e "\npip is required but it's not installed." 8 | echo >&2 -e "You can install it by running the following command:\n" 9 | echo >&2 "wget https://bootstrap.pypa.io/get-pip.py --output-document=get-pip.py; chmod +x get-pip.py; sudo -H python3 get-pip.py" 10 | echo >&2 -e "\n" 11 | echo >&2 -e "\nFor more information, see pip documentation: https://pip.pypa.io/en/latest/" 12 | exit 1; 13 | } 14 | 15 | virtualenv --version >/dev/null 2>&1 || { 16 | echo >&2 -e "\nvirtualenv is required but it's not installed." 17 | echo >&2 -e "You can install it by running the following command:\n" 18 | echo >&2 "sudo -H pip3 install virtualenv" 19 | echo >&2 -e "\n" 20 | echo >&2 -e "\nFor more information, see virtualenv documentation: https://virtualenv.pypa.io/en/latest/" 21 | exit 1; 22 | } 23 | 24 | if [ -z "$VIRTUAL_ENV" ]; then 25 | echo >&2 -e "\nYou need activate a virtualenv first" 26 | echo >&2 -e 'If you do not have a virtualenv created, run the following command to create and automatically activate a new virtualenv named "venv" on current folder:\n' 27 | echo >&2 -e "virtualenv venv --python=\`which python3\`" 28 | echo >&2 -e "\nTo leave/disable the currently active virtualenv, run the following command:\n" 29 | echo >&2 "deactivate" 30 | echo >&2 -e "\nTo activate the virtualenv again, run the following command:\n" 31 | echo >&2 "source venv/bin/activate" 32 | echo >&2 -e "\nFor more information, see virtualenv documentation: https://virtualenv.pypa.io/en/latest/" 33 | echo >&2 -e "\n" 34 | exit 1; 35 | else 36 | 37 | pip install -r $PROJECT_DIR/requirements/local.txt 38 | 39 | fi 40 | -------------------------------------------------------------------------------- /utility/requirements-bionic.apt: -------------------------------------------------------------------------------- 1 | ##basic build dependencies of various Django apps for Ubuntu Bionic 18.04 2 | #build-essential metapackage install: make, gcc, g++, 3 | build-essential 4 | #required to translate 5 | gettext 6 | python3-dev 7 | 8 | ##shared dependencies of: 9 | ##Pillow, pylibmc 10 | zlib1g-dev 11 | 12 | ##Postgresql and psycopg2 dependencies 13 | libpq-dev 14 | 15 | ##Pillow dependencies 16 | libtiff5-dev 17 | libjpeg8-dev 18 | libfreetype6-dev 19 | liblcms2-dev 20 | libwebp-dev 21 | 22 | ##django-extensions 23 | libgraphviz-dev 24 | -------------------------------------------------------------------------------- /utility/requirements-buster.apt: -------------------------------------------------------------------------------- 1 | ##basic build dependencies of various Django apps for Debian Jessie 10.x 2 | #build-essential metapackage install: make, gcc, g++, 3 | build-essential 4 | #required to translate 5 | gettext 6 | python3-dev 7 | 8 | ##shared dependencies of: 9 | ##Pillow, pylibmc 10 | zlib1g-dev 11 | 12 | ##Postgresql and psycopg2 dependencies 13 | libpq-dev 14 | 15 | ##Pillow dependencies 16 | libtiff5-dev 17 | libjpeg62-turbo-dev 18 | libfreetype6-dev 19 | liblcms2-dev 20 | libwebp-dev 21 | 22 | ##django-extensions 23 | libgraphviz-dev 24 | -------------------------------------------------------------------------------- /utility/requirements-jessie.apt: -------------------------------------------------------------------------------- 1 | ##basic build dependencies of various Django apps for Debian Jessie 8.x 2 | #build-essential metapackage install: make, gcc, g++, 3 | build-essential 4 | #required to translate 5 | gettext 6 | python3-dev 7 | 8 | ##shared dependencies of: 9 | ##Pillow, pylibmc 10 | zlib1g-dev 11 | 12 | ##Postgresql and psycopg2 dependencies 13 | libpq-dev 14 | 15 | ##Pillow dependencies 16 | libtiff5-dev 17 | libjpeg62-turbo-dev 18 | libfreetype6-dev 19 | liblcms2-dev 20 | libwebp-dev 21 | 22 | ##django-extensions 23 | graphviz-dev 24 | -------------------------------------------------------------------------------- /utility/requirements-stretch.apt: -------------------------------------------------------------------------------- 1 | ##basic build dependencies of various Django apps for Debian Jessie 9.x 2 | #build-essential metapackage install: make, gcc, g++, 3 | build-essential 4 | #required to translate 5 | gettext 6 | python3-dev 7 | 8 | ##shared dependencies of: 9 | ##Pillow, pylibmc 10 | zlib1g-dev 11 | 12 | ##Postgresql and psycopg2 dependencies 13 | libpq-dev 14 | 15 | ##Pillow dependencies 16 | libtiff5-dev 17 | libjpeg62-turbo-dev 18 | libfreetype6-dev 19 | liblcms2-dev 20 | libwebp-dev 21 | 22 | ##django-extensions 23 | graphviz-dev 24 | -------------------------------------------------------------------------------- /utility/requirements-trusty.apt: -------------------------------------------------------------------------------- 1 | ##basic build dependencies of various Django apps for Ubuntu Trusty 14.04 2 | #build-essential metapackage install: make, gcc, g++, 3 | build-essential 4 | #required to translate 5 | gettext 6 | python3-dev 7 | 8 | ##shared dependencies of: 9 | ##Pillow, pylibmc 10 | zlib1g-dev 11 | 12 | ##Postgresql and psycopg2 dependencies 13 | libpq-dev 14 | 15 | ##Pillow dependencies 16 | libtiff4-dev 17 | libjpeg8-dev 18 | libfreetype6-dev 19 | liblcms1-dev 20 | libwebp-dev 21 | 22 | ##django-extensions 23 | graphviz-dev 24 | -------------------------------------------------------------------------------- /utility/requirements-xenial.apt: -------------------------------------------------------------------------------- 1 | ##basic build dependencies of various Django apps for Ubuntu Xenial 16.04 2 | #build-essential metapackage install: make, gcc, g++, 3 | build-essential 4 | #required to translate 5 | gettext 6 | python3-dev 7 | 8 | ##shared dependencies of: 9 | ##Pillow, pylibmc 10 | zlib1g-dev 11 | 12 | ##Postgresql and psycopg2 dependencies 13 | libpq-dev 14 | 15 | ##Pillow dependencies 16 | libtiff5-dev 17 | libjpeg8-dev 18 | libfreetype6-dev 19 | liblcms2-dev 20 | libwebp-dev 21 | 22 | ##django-extensions 23 | graphviz-dev 24 | --------------------------------------------------------------------------------