├── .gitignore ├── .python-version ├── .ruby-version ├── LICENSE.txt ├── Procfile ├── README.md ├── Rakefile ├── applications └── baseapp │ ├── __init__.py │ ├── admin │ ├── __init__.py │ ├── base.py │ └── user.py │ ├── apps.py │ ├── libs │ ├── __init__.py │ └── log_helpers.py │ ├── management │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ ├── baseapp_create_app.py │ │ └── baseapp_create_model.py │ └── template_structures │ │ ├── __init__.py │ │ ├── admins │ │ ├── __init__.py │ │ ├── basemodel.py │ │ ├── django.py │ │ └── softdelete.py │ │ ├── application │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── html.py │ │ ├── urls.py │ │ └── views.py │ │ └── models │ │ ├── __init__.py │ │ ├── basemodel.py │ │ ├── django.py │ │ └── softdelete.py │ ├── middlewares │ ├── __init__.py │ └── locale.py │ ├── migrations │ ├── 0001_create_custom_user.py │ └── __init__.py │ ├── mixins │ ├── __init__.py │ └── html_debug.py │ ├── models │ ├── __init__.py │ ├── base.py │ └── user.py │ ├── static │ └── css │ │ └── index.css │ ├── templatetags │ ├── __init__.py │ └── html_debug.py │ ├── tests │ ├── __init__.py │ ├── base_models.py │ ├── test_basemodel.py │ ├── test_basemodelwithsoftdelete.py │ └── test_user.py │ ├── urls.py │ ├── utils │ ├── __init__.py │ ├── console.py │ ├── numerify.py │ ├── upload_handler.py │ └── urlify.py │ ├── views.py │ └── widgets │ ├── __init__.py │ └── image_file.py ├── config ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── development.example.py │ ├── heroku.py │ └── test.py ├── urls.py └── wsgi.py ├── db └── .gitkeep ├── install.sh ├── locale └── tr │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── manage.py ├── media └── .gitkeep ├── requirements.txt ├── requirements ├── base.pip ├── development.pip └── heroku.pip ├── runtime.txt ├── static └── .gitkeep └── templates └── baseapp ├── base.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__/ 3 | *.py[cod] 4 | *.sqlite3 5 | /venv/ 6 | .env 7 | config/settings/development.py 8 | /media/ 9 | !.gitkeep -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.6.0 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Uğur "vigo" Özyılmazel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn config.wsgi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Django](https://img.shields.io/badge/django-1.11.4-green.svg) 2 | ![Version](https://img.shields.io/badge/version-0.1.3-yellow.svg) 3 | 4 | # Django Project Starter Template 5 | 6 | My custom project starter for Django! I’ll try to support every upcoming 7 | Django releases as much as I can! 8 | 9 | ## Requirements 10 | 11 | - Latest Python 3.6+ runtime environment. 12 | - `pip`, `virtualenv`, `virtualenvwrapper` 13 | - If you like to run Rake Tasks, you need `Ruby` too but not required. 14 | 15 | ## Installation 16 | 17 | Please use `virtualenvwrapper` and create your environment and activate it. 18 | 19 | ```bash 20 | # example 21 | $ mkvirtualenv my_projects_env 22 | # or make it active: 23 | $ workon my_projects_env 24 | ``` 25 | 26 | You need to declare **2 environment** variables. I always put my project 27 | specific environment variables under `virtualenvwrapper`’s `postactivate` 28 | file. Open your `~/.virtualenvs/my_projects_env/bin/postactivate` and add 29 | these lines: 30 | 31 | ```bash 32 | export DJANGO_ENV="development" 33 | export DJANGO_SECRET="YOUR-SECRET-HERE" # will fix it in a second. 34 | ``` 35 | 36 | then; 37 | 38 | ```bash 39 | # for django 1.11.4 40 | $ curl -L https://github.com/vigo/django-project-template/archive/django-1.11.4.zip > template.zip 41 | $ unzip template.zip 42 | $ mv django-project-template-django-1.11.4 my_project && rm template.zip 43 | $ cd my_project/ 44 | $ cp config/settings/development.example.py config/settings/development.py 45 | # development.py is not under revison control 46 | $ pip install -r requirements/development.pip 47 | $ git init # now you can start your own repo! 48 | ``` 49 | 50 | or, you can use installer script: 51 | 52 | ```bash 53 | $ bash <(curl -fsSL https://raw.githubusercontent.com/vigo/django-project-template/master/install.sh) 54 | $ cd YOUR_PROJECT/ 55 | $ pip install -r requirements/development.pip 56 | $ git init # now you can start your own repo! 57 | ``` 58 | 59 | This template comes with custom User model. Please take a look at it. If you 60 | need to add/change fields, please do so. If you change anything, please run `makemigrations` 61 | to keep track of your db. Then continue to work: 62 | 63 | ```bash 64 | $ python manage.py migrate 65 | $ python manage.py createsuperuser 66 | # enter: Email, First Name, Last Name and password 67 | $ python manage.py runserver_plus # or 68 | $ rake 69 | ``` 70 | 71 | Now, please generate your secret via: 72 | 73 | ```bash 74 | $ python manage.py generate_secret_key 75 | ``` 76 | 77 | and fix your `~/.virtualenvs/my_projects_env/bin/postactivate` 78 | 79 | --- 80 | 81 | ## Features 82 | 83 | - Custom `User` Model 84 | - Custom `BaseModel` 85 | - Custom `BaseModelWithSoftDelete` 86 | - Custom manager for `BaseModel` and `BaseModelWithSoftDelete` 87 | - More useful Django Application structure! 88 | - Settings abstraction: Development / Production / Heroku / Test 89 | - Requirement abstraction depending on your environment! 90 | - Custom logger and log formatters 91 | - App and Model creator management commands 92 | - Custom Locale middleware 93 | - Debug Mixins for your HTML templates 94 | - Handy utils: `console`, `console.dir()`, `numerify`, `urlify`, `save_file` 95 | - File widget for Django Admin: `AdminImageFileWidget` 96 | 97 | --- 98 | 99 | ## Quick Start 100 | 101 | Let’s create `blog` application. We’ll have two models. `Post` and `Category`. 102 | First, create application: 103 | 104 | ```bash 105 | $ python manage.py baseapp_create_app blog 106 | "blog" application created. 107 | 108 | 109 | - Do not forget to add your `blog` to `INSTALLED_APPS` under `config/settings/base.py`: 110 | 111 | INSTALLED_APPS += [ 112 | 'blog', 113 | ] 114 | 115 | - Do not forget to fix your `config/settings/urls.py`: 116 | 117 | urlpatterns = [ 118 | # ... 119 | # this is just an example! 120 | url(r'^__blog__/', include('blog.urls', namespace='blog')), 121 | # .. 122 | ] 123 | 124 | 125 | ``` 126 | 127 | Fix your `config/settings/base.py`, add this newly created app to your `INSTALLED_APPS`: 128 | 129 | ```python 130 | # config/settings/base.py 131 | : 132 | : 133 | AUTH_USER_MODEL = 'baseapp.User' 134 | 135 | INSTALLED_APPS += [ 136 | 'blog', 137 | ] 138 | 139 | ``` 140 | 141 | Now, if you fix your `config/settings/urls.py` you’ll be able to see demo 142 | pages for your app: 143 | 144 | ```python 145 | # config/settings/urls.py 146 | : 147 | : 148 | urlpatterns = [ 149 | # ... 150 | url(r'^__blog__/', include('blog.urls', namespace='blog')), 151 | # .. 152 | ] 153 | ``` 154 | 155 | Now run server and call `http://127.0.0.1:8000/__blog__/`: 156 | 157 | ```bash 158 | $ python manage.py runserver 159 | ``` 160 | 161 | You’ll see `Hello from Blog` page and If you check `blog/views.py` you’ll see 162 | and example usage of `HtmlDebugMixin` and `console` util. 163 | 164 | ```python 165 | from django.views.generic.base import TemplateView 166 | 167 | from baseapp.mixins import HtmlDebugMixin 168 | from baseapp.utils import console 169 | 170 | 171 | console.configure( 172 | source='blog/views.py', 173 | ) 174 | 175 | 176 | class BlogView(HtmlDebugMixin, TemplateView): 177 | template_name = 'blog/index.html' 178 | 179 | def get_context_data(self, **kwargs): 180 | self.hdbg('Hello from hdbg') 181 | kwargs = super().get_context_data(**kwargs) 182 | console.dir(self.request.user) 183 | return kwargs 184 | 185 | ``` 186 | 187 | Let’s look at our `blog` application structure: 188 | 189 | applications/blog/ 190 | ├── admin 191 | │   └── __init__.py 192 | ├── migrations 193 | │   └── __init__.py 194 | ├── models 195 | │   └── __init__.py 196 | ├── apps.py 197 | ├── urls.py 198 | └── views.py 199 | 200 | Now lets add `Post` model with **soft-delete** feature! 201 | 202 | ```bash 203 | $ python manage.py baseapp_create_model blog Post softdelete 204 | models/post.py created. 205 | admin/post.py created. 206 | Post model added to models/__init__.py 207 | Post model added to admin/__init__.py 208 | 209 | 210 | `Post` related files created successfully: 211 | 212 | - `blog/models/post.py` 213 | - `blog/admin/post.py` 214 | 215 | Please check your models before running `makemigrations` ok? 216 | 217 | ``` 218 | 219 | This creates `blog/models/post.py` and `blog/admin/post.py` files: 220 | 221 | ```python 222 | # blog/models/post.py 223 | 224 | from django.utils.translation import ugettext_lazy as _ 225 | from django.db import models 226 | 227 | from baseapp.models import BaseModelWithSoftDelete 228 | 229 | 230 | __all__ = [ 231 | 'Post', 232 | ] 233 | 234 | 235 | class Post(BaseModelWithSoftDelete): 236 | title = models.CharField( 237 | max_length=255, 238 | verbose_name=_('title'), 239 | ) 240 | 241 | class Meta: 242 | app_label = 'blog' 243 | verbose_name = _('Post') 244 | verbose_name_plural = _('Post') 245 | 246 | def __str__(self): 247 | return self.title 248 | 249 | 250 | ``` 251 | 252 | and `Category` model: 253 | 254 | ```bash 255 | $ python manage.py baseapp_create_model blog Category softdelete 256 | models/category.py created. 257 | admin/category.py created. 258 | Category model added to models/__init__.py 259 | Category model added to admin/__init__.py 260 | 261 | 262 | `Category` related files created successfully: 263 | 264 | - `blog/models/category.py` 265 | - `blog/admin/category.py` 266 | 267 | Please check your models before running `makemigrations` ok? 268 | 269 | ``` 270 | 271 | Now It’s time to fix our models by hand! 272 | 273 | ```python 274 | # blog/models/post.py 275 | 276 | from django.conf import settings 277 | from django.utils.translation import ugettext_lazy as _ 278 | from django.db import models 279 | 280 | from baseapp.models import BaseModelWithSoftDelete 281 | 282 | 283 | __all__ = [ 284 | 'Post', 285 | ] 286 | 287 | 288 | class Post(BaseModelWithSoftDelete): 289 | author = models.ForeignKey( 290 | to=settings.AUTH_USER_MODEL, 291 | on_delete=models.CASCADE, 292 | related_name='posts', 293 | verbose_name=_('Author'), 294 | ) 295 | category = models.ForeignKey( 296 | to='Category', 297 | on_delete=models.CASCADE, 298 | related_name='posts', 299 | verbose_name=_('Category'), 300 | ) 301 | title = models.CharField( 302 | max_length=255, 303 | verbose_name=_('Title'), 304 | ) 305 | body = models.TextField( 306 | verbose_name=_('Body'), 307 | ) 308 | 309 | class Meta: 310 | app_label = 'blog' 311 | verbose_name = _('Post') 312 | verbose_name_plural = _('Post') 313 | 314 | def __str__(self): 315 | return self.title 316 | 317 | ``` 318 | 319 | We’ll keep `blog/models/category.py` same, `Category` will have only `title` 320 | field: 321 | 322 | ```python 323 | # blog/models/category.py 324 | 325 | from django.utils.translation import ugettext_lazy as _ 326 | from django.db import models 327 | 328 | from baseapp.models import BaseModelWithSoftDelete 329 | 330 | 331 | __all__ = [ 332 | 'Category', 333 | ] 334 | 335 | 336 | class Category(BaseModelWithSoftDelete): 337 | title = models.CharField( 338 | max_length=255, 339 | verbose_name=_('title'), 340 | ) 341 | 342 | class Meta: 343 | app_label = 'blog' 344 | verbose_name = _('Category') 345 | verbose_name_plural = _('Category') 346 | 347 | def __str__(self): 348 | return self.title 349 | 350 | 351 | ``` 352 | 353 | 354 | Now It’s time to create migrations: 355 | 356 | ```bash 357 | $ python manage.py makemigrations --name create_post_and_category 358 | Migrations for 'blog': 359 | applications/blog/migrations/0001_create_post_and_category.py 360 | - Create model Category 361 | - Create model Post 362 | ``` 363 | 364 | Now migrate! 365 | 366 | ```bash 367 | $ python manage.py migrate 368 | Operations to perform: 369 | Apply all migrations: admin, auth, baseapp, blog, contenttypes, sessions 370 | Running migrations: 371 | Applying blog.0001_create_post_and_category... OK 372 | ``` 373 | 374 | Now time to run server and dive in to admin page! 375 | 376 | ```bash 377 | $ python manage.py runserver 378 | Performing system checks... 379 | 380 | System check identified no issues (0 silenced). 381 | September 21, 2017 - 13:34:54 382 | Django version 1.11.4, using settings 'config.settings.development' 383 | Starting development server at http://127.0.0.1:8000/ 384 | Quit the server with CONTROL-C. 385 | ``` 386 | 387 | Open `http://127.0.0.1:8000/admin/` and use your superuser credentials. 388 | 389 | --- 390 | 391 | ## Project File/Folder Structure 392 | 393 | What I’ve changed ? 394 | 395 | - All Django apps live under `applications/` folder. 396 | - All of the models live under `models/` folder. 397 | - All of the admin files live under `admin/` folder. 398 | - Every app should contain It’s own `urls.py`. 399 | - All settings related files will live under `config/settings/` folder. 400 | - Every environment has It’s own setting such as `config/settings/development.py`. 401 | - Every environment/settings can have It’s own package/module requirements. 402 | - All of the templates live under basedir’s `templates/APP_NAME` folder. 403 | - All of the locales live under basedir’s `locale/LANG/...` folder. 404 | - Lastly, Ruby and Python can be friends in a Django Project! 405 | 406 | Here is directory/file structure: 407 | 408 | . 409 | ├── applications 410 | │   └── baseapp 411 | │   ├── admin 412 | │   ├── libs 413 | │   ├── management 414 | │   ├── middlewares 415 | │   ├── migrations 416 | │   ├── mixins 417 | │   ├── models 418 | │   ├── static 419 | │   ├── templatetags 420 | │   ├── tests 421 | │   ├── utils 422 | │   ├── widgets 423 | │   ├── __init__.py 424 | │   ├── apps.py 425 | │   ├── urls.py 426 | │   └── views.py 427 | ├── config 428 | │   ├── settings 429 | │   │   ├── __init__.py 430 | │   │   ├── base.py 431 | │   │   ├── development.example.py 432 | │   │   ├── development.py 433 | │   │   ├── heroku.py 434 | │   │   └── test.py 435 | │   ├── __init__.py 436 | │   ├── urls.py 437 | │   └── wsgi.py 438 | ├── db 439 | │   └── development.sqlite3 440 | ├── locale 441 | │   └── tr 442 | │   └── LC_MESSAGES 443 | ├── media 444 | │   └── avatar 445 | ├── requirements 446 | │   ├── base.pip 447 | │   ├── development.pip 448 | │   └── heroku.pip 449 | ├── static 450 | ├── templates 451 | │   └── baseapp 452 | │   ├── base.html 453 | │   └── index.html 454 | ├── LICENSE.txt 455 | ├── Procfile 456 | ├── README.md 457 | ├── Rakefile 458 | ├── manage.py 459 | ├── requirements.txt 460 | └── runtime.txt 461 | 462 | --- 463 | 464 | ## Settings and Requirements Abstraction 465 | 466 | By default, `manage.py` looks for `DJANGO_ENV` environment variable. Builds 467 | `DJANGO_SETTINGS_MODULE` environment variable according to `DJANGO_ENV` variable. 468 | If your `DJANGO_ENV` environment variable is set to `production`, this means that 469 | you are running `config/settings/production.py`. 470 | 471 | Also `config/wsgi.py` looks for `DJANGO_ENV` environment variable too. For 472 | example, If you want to deploy this application to **HEROKU**, you need 473 | `config/settings/heroku.py` and must add config variable `DJANGO_ENV` and set 474 | it to `heroku` on HEROKU site. (*You’ll find more information further below*) 475 | 476 | All the other settings files (*according to environment*) imports 477 | `config/settings/base.py` and gets everything from it. `development.py` is 478 | un-tracked/git-ignored file. Original file is `development.example.py`. You 479 | need to create a copy of it! (*if you follow along from the beginning, you’ve already did this*) 480 | 481 | All the base/common required Python packages/modules are defined under `requirements/base.pip`: 482 | 483 | ```python 484 | Django==1.11.4 485 | Pillow==4.2.1 486 | ``` 487 | 488 | ### `development.py` 489 | 490 | Logging is enabled. In `CUSTOM_LOGGER_OPTIONS`, you can specify un-wanted file 491 | types not to log your dev-console. You can un-comment `django.db.backends` if 492 | you want to see the SQL queries. Example: 493 | 494 | ```python 495 | LOGGING = { 496 | : 497 | : 498 | 'loggers': { 499 | : 500 | : 501 | 'django.db.backends': { 502 | 'handlers': ['console_sql'], 503 | 'level': 'DEBUG', 504 | }, 505 | 506 | } 507 | } 508 | ``` 509 | 510 | By default, this template ships with 2 awesome/handy Django packages: 511 | 512 | - [Django Extensions][01] 513 | - [Django Debug Toolbar][02] 514 | 515 | We are using `Werkzeug` as Django server and overriding It’s logging formats. 516 | Django Extension adds great functionalities: 517 | 518 | - `admin_generator` 519 | - `clean_pyc` 520 | - `clear_cache` 521 | - `compile_pyc` 522 | - `create_app` 523 | - `create_command` 524 | - `create_jobs` 525 | - `create_template_tags` 526 | - `delete_squashed_migrations` 527 | - `describe_form` 528 | - `drop_test_database` 529 | - `dumpscript` 530 | - `export_emails` 531 | - `find_template` 532 | - `generate_secret_key` 533 | - `graph_models` 534 | - `mail_debug` 535 | - `notes` 536 | - `passwd` 537 | - `pipchecker` 538 | - `print_settings` 539 | - `print_user_for_session` 540 | - `reset_db` 541 | - `runjob` 542 | - `runjobs` 543 | - `runprofileserver` 544 | - `runscript` 545 | - `runserver_plus` 546 | - `set_default_site` 547 | - `set_fake_emails` 548 | - `set_fake_passwords` 549 | - `shell_plus` 550 | - `show_template_tags` 551 | - `show_templatetags` 552 | - `show_urls` 553 | - `sqlcreate` 554 | - `sqldiff` 555 | - `sqldsn` 556 | - `sync_s3` 557 | - `syncdata` 558 | - `unreferenced_files` 559 | - `update_permissions` 560 | - `validate_templates` 561 | 562 | One of my favorite: `python manage.py show_urls` :) 563 | 564 | `AUTH_PASSWORD_VALIDATORS` are removed for development purposes. You can enter 565 | simple passwords such as `1234`. `MEDIA_ROOT` is set to basedir’s `media` folder, 566 | `STATICFILES_DIRS` includes basedir’s `static` folder. 567 | 568 | All the required modules are defined under `requirements/development.pip`: 569 | 570 | ```python 571 | # requirements/development.pip 572 | -r base.pip 573 | ipython==6.1.0 574 | django-extensions==1.9.0 575 | Werkzeug==0.12.2 576 | django-debug-toolbar==1.8 577 | ``` 578 | 579 | ### `test.py` 580 | 581 | Basic settings for running tests. 582 | 583 | ### `heroku.py` 584 | 585 | You can deploy your app to HEROKU super easy. Just set your `ALLOWED_HOSTS`. 586 | Add your heroku domain here: 587 | 588 | ```python 589 | ALLOWED_HOSTS = [ 590 | 'lit-eyrie-63238.herokuapp.com', # example heroku domain 591 | ] 592 | ``` 593 | 594 | All the required modules are defined under `requirements/heroku.pip`: 595 | 596 | ```python 597 | # requirements/heroku.pip 598 | -r base.pip 599 | gunicorn==19.7.1 600 | psycopg2==2.7.3 601 | dj-database-url==0.4.2 602 | whitenoise==3.3.0 603 | ``` 604 | 605 | By default, Heroku requires `requirements.txt`. Therefore we have it too :) 606 | 607 | ```python 608 | # requirements.txt 609 | -r requirements/heroku.pip 610 | ``` 611 | 612 | Heroku also requires `Procfile` and `runtime.txt`. Both provided in the basedir. 613 | Don’t forget to create heroku config variables: 614 | 615 | ```bash 616 | $ heroku login 617 | $ heroku apps:create 618 | $ heroku addons:create heroku-postgresql:hobby-dev 619 | $ heroku config:set DJANGO_ENV="heroku" 620 | $ heroku config:set DJANGO_SECRET='YOUR_GENERATED_RANDOM_SECRET' 621 | $ heroku config:set WEB_CONCURRENCY=3 622 | $ git push heroku master 623 | $ heroku run python manage.py migrate 624 | $ heroku run python manage.py createsuperuser 625 | ``` 626 | 627 | ### Others 628 | 629 | If you are using different platform or OS, such as Ubuntu or your custom 630 | servers, you can follow the settings and requirements conventions. If you name 631 | it `production`, create your `config/settings/production.py` and 632 | `requirements/production.pip`. You must set you `DJANGO_ENV` to `production` 633 | and don’t forget to set `DJANGO_ENV` and `DJANGO_SECRET` on your production 634 | server! 635 | 636 | --- 637 | 638 | ## `User` model 639 | 640 | This is custom model which uses `AbstractBaseUser` and `PermissionsMixin`. 641 | Fields are: 642 | 643 | - `created_at` 644 | - `updated_at` 645 | - `email` 646 | - `first_name` 647 | - `middle_name` (optional) 648 | - `last_name` 649 | - `avatar` (optional) 650 | - `is_active` 651 | - `is_staff` 652 | - `is_superuser` 653 | 654 | Username field is set to: `email`. Your users will login using their email’s 655 | and password’s by default. You can modify everything if you like to. This also 656 | mimics like default User model of Django. Available methods are: 657 | 658 | - `get_short_name` 659 | - `get_full_name` 660 | 661 | --- 662 | 663 | ## `BaseModel` 664 | 665 | This is a common model. By default, `BaseModel` contains these fields: 666 | 667 | - `created_at` 668 | - `updated_at` 669 | - `status` 670 | 671 | Also has custom manager called: `objects_bm`. There are 4 basic status types: 672 | 673 | ```python 674 | STATUS_OFFLINE = 0 675 | STATUS_ONLINE = 1 676 | STATUS_DELETED = 2 677 | STATUS_DRAFT = 3 678 | ``` 679 | 680 | Custom manager has custom querysets against these statuses such as: 681 | 682 | ```python 683 | >>> Post.objects_bm.deleted() # filters: status = STATUS_DELETED 684 | >>> Post.objects_bm.actives() # filters: status = STATUS_ONLINE 685 | >>> Post.objects_bm.offlines() # filters: status = STATUS_OFFLINE 686 | >>> Post.objects_bm.drafts() # filters: status = STATUS_DRAFT 687 | ``` 688 | 689 | ## `BaseModelWithSoftDelete` 690 | 691 | This model inherits from `BaseModel` and provides fake deletion which is 692 | probably called **SOFT DELETE**. Works with related objects who has 693 | `on_delete` option is set to `models.CASCADE`. This means, when you call 694 | model’s `delete()` method or QuerySet’s `delete()` method, it acts like delete 695 | action but never deletes the data. 696 | 697 | Just sets the status field to `STATUS_DELETED` and sets `deleted_at` field to 698 | **NOW**. 699 | 700 | This works exactly like Django’s `delete()`. Broadcasts `pre_delete` and 701 | `post_delete` signals and returns the number of objects marked as deleted and 702 | a dictionary with the number of deletion-marks per object type. 703 | 704 | ```python 705 | >>> Post.objects_bm.all() 706 | 707 | SELECT "blog_post"."id", 708 | "blog_post"."created_at", 709 | "blog_post"."updated_at", 710 | "blog_post"."status", 711 | "blog_post"."deleted_at", 712 | "blog_post"."author_id", 713 | "blog_post"."category_id", 714 | "blog_post"."title", 715 | "blog_post"."body" 716 | FROM "blog_post" 717 | LIMIT 21 718 | 719 | Execution time: 0.000135s [Database: default] 720 | 721 | , , ]> 722 | 723 | >>> Category.objects_bm.all() 724 | 725 | SELECT "blog_category"."id", 726 | "blog_category"."created_at", 727 | "blog_category"."updated_at", 728 | "blog_category"."status", 729 | "blog_category"."deleted_at", 730 | "blog_category"."title" 731 | FROM "blog_category" 732 | WHERE "blog_category"."deleted_at" IS NULL 733 | LIMIT 21 734 | 735 | ]> 736 | 737 | >>> Category.objects_bm.delete() 738 | (4, {'blog.Category': 1, 'blog.Post': 3}) 739 | 740 | >>> Category.objects_bm.all() 741 | # rows are still there! don’t panic! 742 | 743 | >>> Category.objects.all() 744 | ]> 745 | 746 | ``` 747 | 748 | `BaseModelWithSoftDeleteQuerySet` has these query options according to 749 | `status` field: 750 | 751 | - `.all()` 752 | - `.delete()` 753 | - `.undelete()` 754 | - `.deleted()` 755 | 756 | When soft-delete enabled (*during model creation*), Django admin will 757 | automatically use `BaseAdminWithSoftDelete` which is inherited from: 758 | `BaseAdmin` <- `admin.ModelAdmin`. 759 | 760 | --- 761 | 762 | ## `BaseAdmin`, `BaseAdminWithSoftDelete` 763 | 764 | Inherits from `admin.ModelAdmin`. By default, adds `status` to `list_filter`. 765 | You can disable this via setting `sticky_list_filter = None`. When model is 766 | created with `rake new:model...` or from management command, admin file is 767 | automatically generated. 768 | 769 | Example for `Post` model admin. 770 | 771 | ```python 772 | from django.contrib import admin 773 | 774 | from baseapp.admin import BaseAdminWithSoftDelete 775 | 776 | from ..models import Post 777 | 778 | 779 | __all__ = [ 780 | 'PostAdmin', 781 | ] 782 | 783 | 784 | class PostAdmin(BaseAdminWithSoftDelete): 785 | # sticky_list_filter = None 786 | # hide_deleted_at = False 787 | pass 788 | 789 | 790 | admin.site.register(Post, PostAdmin) 791 | 792 | ``` 793 | 794 | By default, `deleted_at` excluded from admin form like `created_at` and 795 | `updated_at` fields. You can also override this via `hide_deleted_at` attribute. 796 | Comment/Uncomment lines according to your needs! This works only in `BaseAdminWithSoftDelete`. 797 | 798 | `BaseAdminWithSoftDelete` also comes with special admin action. You can 799 | recover/make active (*undelete*) multiple objects like deleting items. 800 | 801 | --- 802 | 803 | ## Custom logger and log formatters 804 | 805 | Template ships with `CustomWerkzeugLogFormatter` and `CustomSqlLogFormatter`. 806 | Default development server uses [Werkzeug][03]. Logging is customized against 807 | Werkzeug’s output. Example usage: 808 | 809 | ```python 810 | import logging 811 | logger = logging.getLogger('user_logger') # config/setting/development.py 812 | logger.warning('This is Warning') 813 | ``` 814 | 815 | `werkzueg_filter_extenstions_callback` is stands for `CUSTOM_LOGGER_OPTIONS`’s 816 | `hide_these_extensions` settings. 817 | 818 | --- 819 | 820 | ## `CustomLocaleMiddleware` 821 | 822 | This is mostly used for our custom projects. Injects `LANGUAGE_CODE` variable to 823 | `request` object. `/en/path/to/page/` sets `request.LANGUAGE_CODE` to `en` otherwise `tr`. 824 | 825 | ```python 826 | # add this to your settings/base.py 827 | MIDDLEWARE += [ 828 | 'baseapp.middlewares.CustomLocaleMiddleware', 829 | ] 830 | ``` 831 | 832 | --- 833 | 834 | ## `HtmlDebugMixin` 835 | 836 | `HtmlDebugMixin` injects `{{ IS_DEBUG }}` and `{{ LANG }}` template variables 837 | to context. Also with `self.hdbg(arg, arg, arg)` method, you can debug 838 | anything from view to html template... 839 | 840 | ```python 841 | # example: views.py 842 | 843 | from django.views.generic.base import TemplateView 844 | 845 | from baseapp.mixins import HtmlDebugMixin 846 | 847 | class IndexView(HtmlDebugMixin, TemplateView): 848 | template_name = 'index.html' 849 | 850 | def get_context_data(self, **kwargs): 851 | self.hdbg('This', 'is', 'an', 'example', 'of') 852 | self.hdbg('self.hdbg', 'usage') 853 | self.hdbg(self.request.__dict__) 854 | return kwargs 855 | 856 | ``` 857 | 858 | Just add `{% hdbg %}` in to your `templates/index.html`: 859 | 860 | ```django 861 | 862 |

Hello World

863 | {% hdbg %} 864 | ``` 865 | 866 | Outputs to Html: 867 | 868 | ('This', 'is', 'an', 'example', 'of') 869 | ('self.hdbg', 'usage') 870 | ({'COOKIES': {'__utma': '****', 871 | '__utmz': '****', 872 | 'csrftoken': '****', 873 | 'djdt': 'hide', 874 | 'language': 'tr'}, 875 | 'META': {'CSRF_COOKIE': '****', 876 | 'CSRF_COOKIE_USED': True, 877 | 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 878 | 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', 879 | 'HTTP_ACCEPT_LANGUAGE': 'en-us', 880 | 'HTTP_CONNECTION': 'keep-alive', 881 | 'HTTP_COOKIE': '****; ', 882 | 'HTTP_DNT': '1', 883 | 'HTTP_HOST': '127.0.0.1:8000', 884 | 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 885 | 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) ' 886 | 'AppleWebKit/603.3.8 (KHTML, like Gecko) ' 887 | 'Version/10.1.2 Safari/603.3.8', 888 | 'PATH_INFO': '/__baseapp__/', 889 | 'QUERY_STRING': '', 890 | 'REMOTE_ADDR': '127.0.0.1', 891 | 'REMOTE_PORT': 61081, 892 | 'REQUEST_METHOD': 'GET', 893 | 'SCRIPT_NAME': '', 894 | 'SERVER_NAME': '127.0.0.1', 895 | 'SERVER_PORT': '8000', 896 | 'SERVER_PROTOCOL': 'HTTP/1.1', 897 | 'SERVER_SOFTWARE': 'Werkzeug/0.12.2', 898 | 'werkzeug.request': , 899 | 'wsgi.errors': <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>, 900 | 'wsgi.input': <_io.BufferedReader name=6>, 901 | 'wsgi.multiprocess': False, 902 | 'wsgi.multithread': False, 903 | 'wsgi.run_once': False, 904 | 'wsgi.url_scheme': 'http', 905 | 'wsgi.version': (1, 0)}, 906 | '_cached_user': , 907 | '_messages': , 908 | '_post_parse_error': False, 909 | '_read_started': False, 910 | '_stream': , 911 | 'content_params': {}, 912 | 'content_type': '', 913 | 'csrf_processing_done': True, 914 | 'environ': {'CSRF_COOKIE': '****', 915 | 'CSRF_COOKIE_USED': True, 916 | 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 917 | 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', 918 | 'HTTP_ACCEPT_LANGUAGE': 'en-us', 919 | 'HTTP_CONNECTION': 'keep-alive', 920 | 'HTTP_COOKIE': '****', 921 | 'HTTP_DNT': '1', 922 | 'HTTP_HOST': '127.0.0.1:8000', 923 | 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 924 | 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X ' 925 | '10_12_6) AppleWebKit/603.3.8 (KHTML, like ' 926 | 'Gecko) Version/10.1.2 Safari/603.3.8', 927 | 'PATH_INFO': '/__baseapp__/', 928 | 'QUERY_STRING': '', 929 | 'REMOTE_ADDR': '127.0.0.1', 930 | 'REMOTE_PORT': 61081, 931 | 'REQUEST_METHOD': 'GET', 932 | 'SCRIPT_NAME': '', 933 | 'SERVER_NAME': '127.0.0.1', 934 | 'SERVER_PORT': '8000', 935 | 'SERVER_PROTOCOL': 'HTTP/1.1', 936 | 'SERVER_SOFTWARE': 'Werkzeug/0.12.2', 937 | 'werkzeug.request': , 938 | 'wsgi.errors': <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'>, 939 | 'wsgi.input': <_io.BufferedReader name=6>, 940 | 'wsgi.multiprocess': False, 941 | 'wsgi.multithread': False, 942 | 'wsgi.run_once': False, 943 | 'wsgi.url_scheme': 'http', 944 | 'wsgi.version': (1, 0)}, 945 | 'method': 'GET', 946 | 'path': '/__baseapp__/', 947 | 'path_info': '/__baseapp__/', 948 | 'resolver_match': ResolverMatch(func=baseapp.views.IndexView, args=(), kwargs={}, url_name=index, app_names=[], namespaces=['baseapp']), 949 | 'session': , 950 | 'user': >},) 951 | 952 | --- 953 | 954 | ## `baseapp.utils.console` 955 | 956 | Do you need to debug an object from the View or anywhere from your Python 957 | script? Sometimes you need to print out some variable(s) or values to console 958 | and you want to keep it safe right? `print()` is very dangerous if you forget 959 | on production server. 960 | 961 | `console()`, `console.dir()` they both work only under `DEBUG = True` mode. 962 | 963 | ```python 964 | # example: views.py 965 | 966 | from baseapp.utils import console 967 | 968 | class IndexView(TemplateView): 969 | def get_context_data(self, **kwargs): 970 | kwargs = super().get_context_data(**kwargs) 971 | console('Hello', 'World') 972 | console.dir(self.request.user) 973 | return kwargs 974 | ``` 975 | 976 | Now `console.dir()` outputs to terminal: 977 | 978 | instance of AnonymousUser | ** 979 | ( { 'arg': ( >,), 980 | 'instance_attributes': ['_setupfunc', '_wrapped'], 981 | 'internal_methods': [ '__class__', '__delattr__', '__dict__', 982 | '__dir__', '__doc__', '__eq__', '__format__', 983 | '__ge__', '__getattribute__', '__gt__', 984 | '__hash__', '__init__', '__init_subclass__', 985 | '__le__', '__lt__', '__module__', '__ne__', 986 | '__new__', '__reduce__', '__reduce_ex__', 987 | '__repr__', '__setattr__', '__sizeof__', 988 | '__str__', '__subclasshook__', '__weakref__'], 989 | 'private_methods': ['_groups', '_user_permissions'], 990 | 'public_attributes': [ 'check_password', 'delete', 991 | 'get_all_permissions', 992 | 'get_group_permissions', 'get_username', 993 | 'groups', 'has_module_perms', 'has_perm', 994 | 'has_perms', 'id', 'is_active', 995 | 'is_anonymous', 'is_authenticated', 996 | 'is_staff', 'is_superuser', 'pk', 'save', 997 | 'set_password', 'user_permissions', 998 | 'username'], 999 | 'public_methods': ['_setupfunc']},) 1000 | ******************************************************************************** 1001 | 1002 | You can set defaults for `console`: 1003 | 1004 | ```python 1005 | from baseapp.utils import console 1006 | 1007 | console.configure( 1008 | char='x', # banners will use `x` character 1009 | source='console.py', # banner title will be `console.py` 1010 | width=8, # output width will wrap to 8 chars (demo purpose) 1011 | indent=8, # 8 characters will be userd for indention (demo purpose) 1012 | color='white', # banner color will be: white 1013 | ) 1014 | 1015 | console.configure(color='default') # resets color 1016 | console('Hello again...') 1017 | ``` 1018 | 1019 | There are few basic color options available: 1020 | 1021 | - black 1022 | - red 1023 | - green 1024 | - yellow 1025 | - blue 1026 | - magenta 1027 | - cyan 1028 | - white 1029 | - default 1030 | 1031 | --- 1032 | 1033 | ## `baseapp.utils.numerify` 1034 | 1035 | Little helper for catching **QUERY_STRING** parameters for numerical values: 1036 | 1037 | ```python 1038 | from baseapp.utils import numerify 1039 | 1040 | >>> numerify("1") 1041 | 1 1042 | >>> numerify("1a") 1043 | -1 1044 | >>> numerify("ab") 1045 | -1 1046 | >>> numerify("abc", default=44) 1047 | 44 1048 | ``` 1049 | 1050 | --- 1051 | 1052 | ## `baseapp.utils.urlify` 1053 | 1054 | Turkish language and Django’s `slugify` are not working well together. This 1055 | little pre-processor will prep string for slugification :) 1056 | 1057 | ```python 1058 | from django.utils.text import slugify 1059 | from baseapp.utils import urlify 1060 | 1061 | >>> slugify(urlify('Merhaba Dünya!')) 1062 | 'merhaba-dunya' 1063 | 1064 | >>> slugify(urlify('Merhaba Dünya! ĞŞİ')) 1065 | 'merhaba-dunya-gsi' 1066 | ``` 1067 | 1068 | --- 1069 | 1070 | ## `baseapp.utils.save_file` 1071 | 1072 | While using `FileField`, sometimes you need to handle uploaded files. In this 1073 | case, you need to use `upload_to` attribute. Take a look at the example in `baseapp/models/user.py`: 1074 | 1075 | ```python 1076 | from baseapp.utils import save_file as custom_save_file 1077 | : 1078 | : 1079 | : 1080 | class User(AbstractBaseUser, PermissionsMixin): 1081 | : 1082 | : 1083 | avatar = models.FileField( 1084 | upload_to=save_user_avatar, 1085 | verbose_name=_('Profile Image'), 1086 | null=True, 1087 | blank=True, 1088 | ) 1089 | : 1090 | : 1091 | ``` 1092 | 1093 | `save_user_avatar` returns `custom_save_file`’s return value. Default 1094 | configuration of for `custom_save_file` is 1095 | `save_file(instance, filename, upload_to='upload/%Y/%m/%d/')`. Uploads are go to 1096 | such as `MEDIA_ROOT/upload/2017/09/21/`... 1097 | 1098 | Make your custom uploads like: 1099 | 1100 | ```python 1101 | from baseapp.utils import save_file as custom_save_file 1102 | 1103 | def my_custom_uploader(instance, filename): 1104 | # do your stuff 1105 | # at the end, call: 1106 | return custom_save_file(instance, filename, upload_to='images/%Y/') 1107 | 1108 | 1109 | class MyModel(models.Model): 1110 | image = models.FileField( 1111 | upload_to='my_custom_uploader', 1112 | verbose_name=_('Profile Image'), 1113 | ) 1114 | 1115 | ``` 1116 | 1117 | ## `AdminImageFileWidget` 1118 | 1119 | Use this widget in your admin forms: 1120 | 1121 | ```python 1122 | from baseapp.widgets import AdminImageFileWidget 1123 | 1124 | class MyAdmin(admin.ModelAdmin): 1125 | formfield_overrides = { 1126 | models.FileField: {'widget': AdminImageFileWidget}, 1127 | } 1128 | 1129 | ``` 1130 | 1131 | This widget uses `Pillow` (*Python Image Library*) which ships with your `base.pip` 1132 | requirements file. Show image preview, width x height if the file is image. 1133 | 1134 | --- 1135 | 1136 | ## Rakefile 1137 | 1138 | If you have Ruby installed, you’ll have lots of handy tasks for the project. 1139 | Type `rake -T` for list of tasks: 1140 | 1141 | ```bash 1142 | $ rake -T 1143 | rake db:migrate[database] # Run migration for given database (default: 'default') 1144 | rake db:roll_back[name_of_application,name_of_migration] # Roll-back (name of application, name of migration) 1145 | rake db:shell # run database shell .. 1146 | rake db:show[name_of_application] # Show migrations for an application (default: 'all') 1147 | rake db:update[name_of_application,name_of_migration,is_empty] # Update migration (name of application, name of migration?, is empty?) 1148 | rake locale:compile # Compile locale dictionary 1149 | rake locale:update # Update locale dictionary 1150 | rake new:application[name_of_application] # Create new Django application 1151 | rake new:model[name_of_application,name_of_model,type_of_model] # Create new Model for given application 1152 | rake run_server # Run server 1153 | rake shell # Run shell+ 1154 | rake test:baseapp # Run test against baseapp 1155 | ``` 1156 | 1157 | Default task is `run_server`. Just type `rake` that’s it! `runserver` uses 1158 | `runserver_plus`. This means you have lots of debugging options! 1159 | 1160 | ### `rake db:migrate[database]` 1161 | 1162 | Migrates database with given database name. Default is `default`. If you like 1163 | to work multiple databases: 1164 | 1165 | ```python 1166 | # config/settings/development.py 1167 | 1168 | DATABASES = { 1169 | 'default': { 1170 | 'ENGINE': 'django.db.backends.sqlite3', 1171 | 'NAME': os.path.join(BASE_DIR, 'db', 'development.sqlite3'), 1172 | }, 1173 | 'my_database': { 1174 | 'ENGINE': 'django.db.backends.sqlite3', 1175 | 'NAME': os.path.join(BASE_DIR, 'db', 'my_database.sqlite3'), 1176 | } 1177 | } 1178 | ``` 1179 | 1180 | You can just call `rake db:migrate` or specify different database like: 1181 | `rake db:migrate[my_database]` :) 1182 | 1183 | ### `rake db:roll_back[name_of_application,name_of_migration]` 1184 | 1185 | Your database must be rollable :) To see available migrations: 1186 | `rake db:roll_back[NAME_OF_YOUR_APPLICATION]`. Look at the list and choose your 1187 | target migration (*example*): `rake db:roll_back[baseapp,0001_create_custom_user]`. 1188 | 1189 | ```bash 1190 | # example scenario 1191 | $ rake db:roll_back[baseapp] 1192 | Please select your migration: 1193 | baseapp 1194 | [X] 0001_create_custom_user 1195 | [X] 0002_post_model 1196 | 1197 | $ rake db:roll_back[baseapp,0001_create_custom_user] 1198 | ``` 1199 | 1200 | ### `rake db:shell` 1201 | 1202 | Runs default database client. 1203 | 1204 | ### `rake db:show[name_of_application]` 1205 | 1206 | Show migrations. Examples: 1207 | 1208 | ```bash 1209 | $ rake db:show # shows everything 1210 | admin 1211 | [X] 0001_initial 1212 | [X] 0002_logentry_remove_auto_add 1213 | auth 1214 | [X] 0001_initial 1215 | [X] 0002_alter_permission_name_max_length 1216 | [X] 0003_alter_user_email_max_length 1217 | [X] 0004_alter_user_username_opts 1218 | [X] 0005_alter_user_last_login_null 1219 | [X] 0006_require_contenttypes_0002 1220 | [X] 0007_alter_validators_add_error_messages 1221 | [X] 0008_alter_user_username_max_length 1222 | baseapp 1223 | [X] 0001_create_custom_user 1224 | blog 1225 | [X] 0001_create_post_and_category 1226 | contenttypes 1227 | [X] 0001_initial 1228 | [X] 0002_remove_content_type_name 1229 | sessions 1230 | [X] 0001_initial 1231 | ``` 1232 | 1233 | or just a specific app: 1234 | 1235 | ```bash 1236 | $ rake db:show[blog] 1237 | blog 1238 | [X] 0001_create_post_and_category 1239 | ``` 1240 | 1241 | ### `rake db:update[name_of_application,name_of_migration,is_empty]` 1242 | 1243 | When you add/change something in the model, you need to create migrations. Use 1244 | this task. Let’s say you have added new field to `Post` model in your `blog` 1245 | app: 1246 | 1247 | ```bash 1248 | $ rake db:update[blog] # automatic migration (example) 1249 | Migrations for 'blog': 1250 | applications/blog/migrations/0003_auto_20170921_1357.py 1251 | - Alter field category on post 1252 | - Alter field title on post 1253 | 1254 | $ rake db:update[blog,add_new_field_to_post] # migration with name (example) 1255 | Migrations for 'blog': 1256 | applications/blog/migrations/0002_add_new_field_to_post.py 1257 | 1258 | $ rake db:update[blog,add_new_field_to_post,yes] # migration with name (example) 1259 | Migrations for 'blog': 1260 | applications/blog/migrations/0002_empty_mig.py 1261 | ``` 1262 | 1263 | ### `rake locale:compile` and `rake locale:update` 1264 | 1265 | When you make changes in your application related to locales, run: `rake locale:update`. 1266 | When you finish editing your `django.po` file, run `rake locale:compile`. 1267 | 1268 | ### `rake new:application[name_of_application]` 1269 | 1270 | Creates new application! 1271 | 1272 | ```bash 1273 | $ rake new:application[blog] 1274 | ``` 1275 | 1276 | ### `rake new:model[name_of_application,name_of_model,type_of_model]` 1277 | 1278 | Creates new model! Available model types are: `django` (default), `basemodel` 1279 | and `softdelete`. 1280 | 1281 | ```bash 1282 | $ rake new:model[blog,Post] # will create model using Django’s `models.Model` 1283 | $ rake new:model[blog,Post,basemodel] # will create model using our `BaseModel` 1284 | $ rake new:model[blog,Post,softdelete] # will create model using our `BaseModelWithSoftDelete` 1285 | ``` 1286 | 1287 | ### `rake shell` 1288 | 1289 | Runs Django repl/shell with use `shell_plus` of [django-extensions][01]. 1290 | `rake shell`. This loads everything to your shell! Also you can see the 1291 | SQL statements while playing in shell. 1292 | 1293 | ### `rake test:baseapp` 1294 | 1295 | Runs tests of baseapp! 1296 | 1297 | --- 1298 | 1299 | ## Tests 1300 | 1301 | ```bash 1302 | $ DJANGO_ENV=test python manage.py test baseapp -v 2 # or 1303 | $ DJANGO_ENV=test python manage.py test baseapp.tests.CustomUserTestCase # single, or 1304 | $ rake test:baseapp 1305 | ``` 1306 | 1307 | --- 1308 | 1309 | ## Notes 1310 | 1311 | If you created models via management command or rake task, you’ll have admin 1312 | file automatically and generated against your model type. If you created a model 1313 | with `BaseModelWithSoftDelete`, you’ll have `BaseAdminWithSoftDelete` set. 1314 | 1315 | `BaseAdminWithSoftDelete` uses `objects_bm` in `get_queryset` and by default, 1316 | you’ll have extra actions and soft delete feature. If you don’t want to use 1317 | `objects_bm` manager, you need to override it manually: 1318 | 1319 | ```python 1320 | # example: blog/admin/post.py 1321 | 1322 | from django.contrib import admin 1323 | 1324 | from baseapp.admin import BaseAdminWithSoftDelete 1325 | 1326 | from ..models import Post 1327 | 1328 | 1329 | __all__ = [ 1330 | 'PostAdmin', 1331 | ] 1332 | 1333 | 1334 | class PostAdmin(BaseAdminWithSoftDelete): 1335 | # sticky_list_filter = None 1336 | # hide_deleted_at = False 1337 | 1338 | def get_queryset(self, request): 1339 | return self.model.objects.get_queryset() # this line! 1340 | 1341 | 1342 | admin.site.register(Post, PostAdmin) 1343 | 1344 | ``` 1345 | 1346 | --- 1347 | 1348 | ## Manual Usage 1349 | 1350 | Let’s assume you need a model called: `Page`. Create a file under `YOUR_APP/models/page.py`: 1351 | 1352 | ```python 1353 | # YOUR_APP/models/page.py 1354 | 1355 | from django.db import models 1356 | 1357 | 1358 | __all__ = [ 1359 | 'Page', 1360 | ] 1361 | 1362 | class Page(models.Model): 1363 | # define your fields here... 1364 | pass 1365 | 1366 | # YOUR_APP/models/__init__.py 1367 | # append: 1368 | from .page import * 1369 | 1370 | ``` 1371 | 1372 | Now make migrations etc... Use it as `from YOUR_APP.models import Page` :) 1373 | 1374 | --- 1375 | 1376 | ## Contributer(s) 1377 | 1378 | * [Uğur "vigo" Özyılmazel](https://github.com/vigo) - Creator, maintainer 1379 | 1380 | --- 1381 | 1382 | ## Contribute 1383 | 1384 | All PR’s are welcome! 1385 | 1386 | 1. `fork` (https://github.com/vigo/django-project-template/fork) 1387 | 1. Create your `branch` (`git checkout -b my-features`) 1388 | 1. `commit` yours (`git commit -am 'added killer options'`) 1389 | 1. `push` your `branch` (`git push origin my-features`) 1390 | 1. Than create a new **Pull Request**! 1391 | 1392 | --- 1393 | 1394 | ## License 1395 | 1396 | This project is licensed under MIT 1397 | 1398 | --- 1399 | 1400 | ## Change Log 1401 | 1402 | **2018-01-25** 1403 | 1404 | - Fix: model generator now creates more meaningfull file names such as `CustomDataPage` => `custom_data_page.py` in `models/` and `admins/` 1405 | - Fix: admin generator now imports (example:) `CustomDataPageAdmin` instead of `CustomDataPage`. 1406 | 1407 | **2017-10-02** 1408 | 1409 | - Added: `created_at` and `updated_at` fields to custom User model. 1410 | 1411 | [01]: https://github.com/django-extensions/django-extensions "Django Extensions" 1412 | [02]: https://django-debug-toolbar.readthedocs.io/en/stable/ "Django Debug Toolbar" 1413 | [03]: http://werkzeug.pocoo.org "Werkzeug" -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => [:run_server] 2 | 3 | task :check_django_environment do 4 | abort "Set DJANGO_ENV variable! via export DJANGO_ENV=..." unless ENV['DJANGO_ENV'] 5 | end 6 | 7 | task :check_development_environment => [:check_django_environment] do 8 | abort "Set DJANGO_ENV to development" unless ENV['DJANGO_ENV'] == 'development' 9 | end 10 | 11 | desc "Run server" 12 | task :run_server => [:check_development_environment] do 13 | system "DJANGO_COLORS='dark' python manage.py runserver_plus --nothreading" 14 | end 15 | 16 | desc "Run shell+" 17 | task :shell => [:check_development_environment] do 18 | system "python manage.py shell_plus --print-sql --ipython" 19 | end 20 | 21 | namespace :new do 22 | desc "Create new Django application" 23 | task :application, [:name_of_application] => [:check_development_environment] do |t, args| 24 | abort "Please provide: 'name_of_application'" unless args.name_of_application 25 | 26 | system "python manage.py baseapp_create_app #{args.name_of_application}" 27 | end 28 | 29 | AVAILABLE_MODEL_TYPES = ['django', 'basemodel', 'softdelete'] 30 | desc "Create new Model for given application" 31 | task :model, [:name_of_application, :name_of_model, :type_of_model] => [:check_development_environment] do |t, args| 32 | args.with_defaults(:type_of_model => "django") 33 | abort "Please provide: 'name_of_application'" unless args.name_of_application 34 | abort "Please provide: 'name_of_model'" unless args.name_of_model 35 | abort "Please provide valide model tyoe: #{AVAILABLE_MODEL_TYPES.join(',')}" unless AVAILABLE_MODEL_TYPES.include?(args.type_of_model) 36 | 37 | system "python manage.py baseapp_create_model #{args.name_of_application} #{args.name_of_model} #{args.type_of_model}" 38 | end 39 | end 40 | 41 | namespace :locale do 42 | desc "Update locale dictionary" 43 | task :update => [:check_development_environment] do 44 | system "python manage.py makemessages -a -s" 45 | end 46 | desc "Compile locale dictionary" 47 | task :compile => [:check_development_environment] do 48 | system "python manage.py compilemessages" 49 | end 50 | end 51 | 52 | namespace :db do 53 | desc "Run migration for given database (default: 'default')" 54 | task :migrate, [:database] => [:check_development_environment] do |t, args| 55 | args.with_defaults(:database => "default") 56 | 57 | puts "Running migration for: #{args.database} database..." 58 | system "python manage.py migrate --database=#{args.database}" 59 | end 60 | 61 | desc "run database shell ..." 62 | task :shell => [:check_development_environment] do 63 | system "python manage.py dbshell" 64 | end 65 | 66 | desc "Show migrations for an application (default: 'all')" 67 | task :show, [:name_of_application] => [:check_development_environment] do |t, args| 68 | args.with_defaults(:name_of_application => "all") 69 | single_application_or_all = " #{args.name_of_application}" 70 | single_application_or_all = "" if args.name_of_application == "all" 71 | system "python manage.py showmigrations#{single_application_or_all}" 72 | end 73 | 74 | desc "Update migration (name of application, name of migration?, is empty?)" 75 | task :update, [:name_of_application, :name_of_migration, :is_empty] => [:check_development_environment] do |t, args| 76 | abort "Please provide: 'name_of_application'" unless args.name_of_application 77 | 78 | args.with_defaults(:name_of_migration => "auto_#{Time.now.strftime('%Y%m%d_%H%M')}") 79 | args.with_defaults(:is_empty => "no") 80 | 81 | name_param = "--name #{args.name_of_migration}" 82 | empty_param = "" 83 | unless args.is_empty == "no" 84 | empty_param = "--empty #{args.name_of_application} " 85 | end 86 | system "python manage.py makemigrations #{empty_param}#{name_param}" 87 | end 88 | 89 | desc "Roll-back (name of application, name of migration)" 90 | task :roll_back, [:name_of_application, :name_of_migration] => [:check_development_environment] do |t, args| 91 | abort "Please provide: 'name_of_application'" unless args.name_of_application 92 | 93 | args.with_defaults(:name_of_migration => nil) 94 | 95 | which_application = args.name_of_application 96 | which_application = "" if args.name_of_application == "all" 97 | 98 | if args.name_of_migration.nil? 99 | puts "Please select your migration:" 100 | system "python manage.py showmigrations #{which_application}" 101 | else 102 | system "python manage.py migrate #{which_application} #{args.name_of_migration}" 103 | end 104 | end 105 | end 106 | 107 | namespace :test do 108 | desc "Run test against baseapp" 109 | task :baseapp do 110 | system "DJANGO_ENV=test python manage.py test baseapp -v 2" 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /applications/baseapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import * 2 | from .base import * 3 | -------------------------------------------------------------------------------- /applications/baseapp/admin/base.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.contrib import admin 3 | 4 | from ..utils import numerify 5 | from ..models import BaseModel 6 | 7 | 8 | __all__ = [ 9 | 'BaseAdmin', 10 | 'BaseAdminWithSoftDelete', 11 | ] 12 | 13 | 14 | class BaseAdmin(admin.ModelAdmin): 15 | sticky_list_filter = ('status',) 16 | 17 | def get_list_filter(self, request): 18 | list_filter = list(super().get_list_filter(request)) 19 | if self.sticky_list_filter: 20 | list_filter = list(self.sticky_list_filter) + list(list_filter) 21 | return list_filter 22 | 23 | 24 | def recover_deleted(modeladmin, request, queryset): 25 | number_of_rows_recovered, recovered_items = queryset.undelete() 26 | if number_of_rows_recovered == 1: 27 | message_bit = _('1 record was') 28 | else: 29 | message_bit = _('%(number_of_rows)s records were') % dict(number_of_rows=number_of_rows_recovered) 30 | message = _('%(message_bit)s successfully marked as active') % dict(message_bit=message_bit) 31 | modeladmin.message_user(request, message) 32 | return None 33 | 34 | 35 | class BaseAdminWithSoftDelete(BaseAdmin): 36 | hide_deleted_at = True 37 | 38 | def get_queryset(self, request): 39 | qs = self.model.objects_bm.get_queryset() 40 | if request.GET.get('status__exact', None): 41 | if numerify(request.GET.get('status__exact')) == BaseModel.STATUS_DELETED: 42 | return qs.deleted() 43 | return qs.all() 44 | 45 | def get_exclude(self, request, obj=None): 46 | excluded = super().get_exclude(request, obj=obj) 47 | exclude = [] if excluded is None else list(excluded) 48 | if self.hide_deleted_at: 49 | exclude.append('deleted_at') 50 | return exclude 51 | 52 | def get_actions(self, request): 53 | existing_actions = super().get_actions(request) 54 | existing_actions.update(dict( 55 | recover_deleted=( 56 | recover_deleted, 57 | 'recover_deleted', 58 | _('Recover selected %(verbose_name_plural)s'), 59 | ) 60 | )) 61 | return existing_actions 62 | -------------------------------------------------------------------------------- /applications/baseapp/admin/user.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db import models 3 | from django.contrib import admin 4 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 5 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 6 | from django.utils.translation import ugettext_lazy as _ 7 | from django.utils.html import format_html 8 | 9 | from ..models import User 10 | from baseapp.widgets import AdminImageFileWidget 11 | 12 | 13 | __all__ = [ 14 | 'UserAdmin', 15 | ] 16 | 17 | 18 | class UserChangeForm(forms.ModelForm): 19 | 20 | password = ReadOnlyPasswordHashField( 21 | label=_('Password'), 22 | help_text=_( 23 | 'Raw passwords are not stored, so there is no way to see this ' 24 | 'user\'s password, but you can change the password using ' 25 | 'this form.' 26 | ), 27 | ) 28 | 29 | class Meta: 30 | model = User 31 | fields = ( 32 | 'email', 33 | 'first_name', 34 | 'last_name', 35 | 'password', 36 | 'is_active', 37 | 'is_staff', 38 | 'is_superuser', 39 | ) 40 | labels = { 41 | 'first_name': _('first name').title(), 42 | 'last_name': _('last name').title(), 43 | } 44 | 45 | def clean_password(self): 46 | return self.initial['password'] 47 | 48 | 49 | class UserCreationForm(forms.ModelForm): 50 | 51 | password1 = forms.CharField( 52 | label=_('Password'), 53 | widget=forms.PasswordInput, 54 | ) 55 | password2 = forms.CharField( 56 | label=_('Password confirmation'), 57 | widget=forms.PasswordInput, 58 | ) 59 | 60 | class Meta: 61 | """ 62 | `fields` property holds only required fields. 63 | """ 64 | 65 | model = User 66 | fields = ('first_name', 'last_name') 67 | labels = { 68 | 'first_name': _('first name').title(), 69 | 'last_name': _('last name').title(), 70 | } 71 | 72 | def clean_password2(self): 73 | password1 = self.cleaned_data.get('password1') 74 | password2 = self.cleaned_data.get('password2') 75 | if password1 and password2 and password1 != password2: 76 | raise forms.ValidationError(_('Passwords don\'t match')) 77 | return password2 78 | 79 | def save(self, commit=True): 80 | user = super(UserCreationForm, self).save(commit=False) 81 | user.set_password(self.cleaned_data['password1']) 82 | if commit: 83 | user.save() 84 | return user 85 | 86 | 87 | class UserAdmin(BaseUserAdmin): 88 | 89 | form = UserChangeForm 90 | add_form = UserCreationForm 91 | 92 | list_display = ('user_profile_image', 'email', 'first_name', 'last_name') 93 | list_display_links = ('email',) 94 | search_fields = ('email', 'first_name', 'middle_name', 'last_name') 95 | ordering = ('email',) 96 | fieldsets = ( 97 | (_('User information'), {'fields': ( 98 | 'email', 99 | 'password', 100 | 'first_name', 101 | 'middle_name', 102 | 'last_name', 103 | 'avatar', 104 | )}), 105 | (_('Permissions'), {'fields': ( 106 | 'is_active', 107 | 'is_staff', 108 | 'is_superuser', 109 | 'groups', 110 | 'user_permissions', 111 | )}), 112 | ) 113 | add_fieldsets = ( 114 | (None, { 115 | 'classes': ('wide',), 116 | 'fields': ( 117 | 'email', 118 | 'first_name', 119 | 'last_name', 120 | 'password1', 121 | 'password2', 122 | ) 123 | }), 124 | ) 125 | formfield_overrides = { 126 | models.FileField: {'widget': AdminImageFileWidget}, 127 | } 128 | 129 | def user_profile_image(self, obj): 130 | if obj.avatar: 131 | return format_html('{}', 132 | obj.avatar.url, 133 | obj.get_full_name(), 134 | ) 135 | else: 136 | return '---' 137 | user_profile_image.short_description = _('Profile Image') 138 | 139 | 140 | admin.site.register(User, UserAdmin) 141 | -------------------------------------------------------------------------------- /applications/baseapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BaseappConfig(AppConfig): 5 | name = 'baseapp' 6 | -------------------------------------------------------------------------------- /applications/baseapp/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/libs/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/libs/log_helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.core.management.color import color_style 6 | 7 | ansi_escape = re.compile(r'\x1b[^m]*m') 8 | 9 | 10 | class CustomWerkzeugLogFormatter(logging.Formatter): 11 | def __init__(self, *args, **kwargs): 12 | self.style = color_style() 13 | super().__init__(*args, **kwargs) 14 | 15 | def status_code(self, message): 16 | match = re.search(' (\d+) -$', message) 17 | if match: 18 | return int(match.groups()[0]) 19 | else: 20 | return None 21 | 22 | def format(self, record): 23 | msg = ansi_escape.sub('', record.msg) 24 | status_code = self.status_code(msg) 25 | 26 | if status_code: 27 | if 200 <= status_code < 300: 28 | msg = self.style.HTTP_SUCCESS(msg) 29 | elif 100 <= status_code < 200: 30 | msg = self.style.HTTP_INFO(msg) 31 | elif status_code == 304: 32 | msg = self.style.HTTP_NOT_MODIFIED(msg) 33 | elif 300 <= status_code < 400: 34 | msg = self.style.HTTP_REDIRECT(msg) 35 | elif status_code == 404: 36 | msg = self.style.HTTP_NOT_FOUND(msg) 37 | elif 400 <= status_code < 500: 38 | msg = self.style.HTTP_BAD_REQUEST(msg) 39 | else: 40 | msg = self.style.HTTP_SERVER_ERROR(msg) 41 | 42 | levelname = record.levelname.lower() 43 | levelstyle = self.style.SUCCESS 44 | record.levelname = '{:.<14}'.format(record.levelname) 45 | 46 | if levelname == 'warning': 47 | levelstyle = self.style.WARNING 48 | elif levelname == 'info': 49 | levelstyle = self.style.HTTP_INFO 50 | elif levelname == 'error': 51 | levelstyle = self.style.ERROR 52 | else: 53 | levelstyle = self.style.NOTICE 54 | 55 | record.levelname = levelstyle(record.levelname) 56 | record.msg = msg 57 | return super().format(record) 58 | 59 | 60 | class CustomSqlLogFormatter(logging.Formatter): 61 | def __init__(self, *args, **kwargs): 62 | self.style = color_style() 63 | super().__init__(*args, **kwargs) 64 | 65 | def format(self, record): 66 | record.levelname = '{:.<14}'.format('SQL') 67 | record.levelname = self.style.HTTP_INFO(record.levelname) 68 | record.sql = self.style.SQL_KEYWORD(record.sql) 69 | return super().format(record) 70 | 71 | 72 | def werkzueg_filter_extenstions_callback(record): 73 | if getattr(settings, 'CUSTOM_LOGGER_OPTIONS', False): 74 | hide_these_extensions = settings.CUSTOM_LOGGER_OPTIONS.get('hide_these_extensions', False) 75 | if hide_these_extensions: 76 | return not any(['.{}'.format(ext) in record.msg for ext in hide_these_extensions]) 77 | else: 78 | return True 79 | -------------------------------------------------------------------------------- /applications/baseapp/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/management/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/management/commands/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/management/commands/baseapp_create_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import errno 3 | import time 4 | 5 | from importlib import import_module 6 | 7 | from django.conf import settings 8 | from django.core.management.base import ( 9 | BaseCommand, 10 | CommandError, 11 | ) 12 | 13 | from baseapp.management.template_structures import application as application_templates 14 | 15 | TEMPLATE_MODELS_INIT = """# from .MODEL_FILE import * 16 | 17 | """ 18 | 19 | TEMPLATE_ADMIN_INIT = """# from .ADMIN_FILE import * 20 | 21 | """ 22 | 23 | APP_DIR_STRUCTURE = { 24 | 'packages': [ 25 | dict(name='admin', files=[ 26 | dict(name='__init__.py', render=TEMPLATE_ADMIN_INIT), 27 | ]), 28 | dict(name='migrations'), 29 | dict(name='models', files=[ 30 | dict(name='__init__.py', render=TEMPLATE_MODELS_INIT), 31 | ]), 32 | ], 33 | 'templates': [ 34 | dict(name='index.html', render=application_templates.TEMPLATE_HTML), 35 | ], 36 | 'files': [ 37 | dict(name='apps.py', render=application_templates.TEMPLATE_APPS), 38 | dict(name='urls.py', render=application_templates.TEMPLATE_URLS), 39 | dict(name='views.py', render=application_templates.TEMPLATE_VIEWS), 40 | ] 41 | } 42 | 43 | USER_REMINDER = """ 44 | 45 | - Do not forget to add your `{app_name}` to `INSTALLED_APPS` under `config/settings/base.py`: 46 | 47 | INSTALLED_APPS += [ 48 | '{app_name}', 49 | ] 50 | 51 | - Do not forget to fix your `config/settings/urls.py`: 52 | 53 | urlpatterns = [ 54 | # ... 55 | # this is just an example! 56 | url(r'^__{app_name}__/', include('{app_name}.urls', namespace='{app_name}')), 57 | # .. 58 | ] 59 | 60 | """ 61 | 62 | 63 | class Command(BaseCommand): 64 | help = ( 65 | 'Creates a custom Django app directory structure for the given app name in ' 66 | '`applications/` directory.' 67 | ) 68 | missing_args_message = 'You must provide an application name.' 69 | 70 | def add_arguments(self, parser): 71 | parser.add_argument('name', nargs=1, type=str, help='Name of your application') 72 | 73 | def handle(self, *args, **options): 74 | app_name = options.pop('name')[0] 75 | 76 | try: 77 | import_module(app_name) 78 | except ImportError: 79 | pass 80 | else: 81 | raise CommandError( 82 | '%r conflicts with the name of an existing Python module and ' 83 | 'cannot be used as an app name. Please try another name.' % app_name 84 | ) 85 | 86 | applications_dir = os.path.join(settings.BASE_DIR, 'applications') 87 | templates_dir = os.path.join(settings.BASE_DIR, 'templates') 88 | new_application_dir = os.path.join(applications_dir, app_name) 89 | 90 | render_params = dict( 91 | app_name_title=app_name.title(), 92 | app_name=app_name, 93 | ) 94 | 95 | self.mkdir(new_application_dir) 96 | 97 | for package in APP_DIR_STRUCTURE.get('packages'): 98 | package_dir = os.path.join(new_application_dir, package.get('name')) 99 | self.mkdir(package_dir) 100 | self.touch(os.path.join(package_dir, '__init__.py')) 101 | if package.get('files', False): 102 | self.generate_files(package.get('files'), package_dir, render_params) 103 | 104 | for template in APP_DIR_STRUCTURE.get('templates'): 105 | template_dir = os.path.join(templates_dir, app_name) 106 | template_html_path = os.path.join(template_dir, template.get('name')) 107 | self.mkdir(template_dir) 108 | self.touch(template_html_path) 109 | if template.get('render', False): 110 | rendered_content = template.get('render').format(**render_params) 111 | self.create_file_with_content(template_html_path, rendered_content) 112 | 113 | self.generate_files(APP_DIR_STRUCTURE.get('files'), new_application_dir, render_params) 114 | self.stdout.write(self.style.SUCCESS('"{}" application created.'.format(app_name))) 115 | self.stdout.write(self.style.NOTICE(USER_REMINDER.format(app_name=app_name))) 116 | 117 | def generate_files(self, files_list, root_path, render_params): 118 | for single_file in files_list: 119 | file_path = os.path.join(root_path, single_file.get('name')) 120 | self.touch(file_path) 121 | if single_file.get('render', False): 122 | rendered_content = single_file.get('render').format(**render_params) 123 | self.create_file_with_content(file_path, rendered_content) 124 | 125 | def mkdir(self, dirname): 126 | try: 127 | os.mkdir(dirname) 128 | except OSError as e: 129 | if e.errno == errno.EEXIST: 130 | message = '"%s" already exists' % dirname 131 | else: 132 | message = e 133 | raise CommandError(message) 134 | 135 | def create_file_with_content(self, filename, content): 136 | with open(filename, 'w') as f: 137 | f.write(content) 138 | 139 | def touch(self, filename): 140 | am_time = time.mktime(time.localtime()) 141 | with open(filename, 'a'): 142 | os.utime(filename, (am_time, am_time)) 143 | -------------------------------------------------------------------------------- /applications/baseapp/management/commands/baseapp_create_model.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | from importlib import import_module 5 | 6 | from django.conf import settings 7 | from django.apps import apps 8 | from django.core.management.base import ( 9 | BaseCommand, 10 | CommandError, 11 | ) 12 | 13 | from baseapp.management.template_structures import admins as admin_templates 14 | from baseapp.management.template_structures import models as model_templates 15 | 16 | 17 | TEMPLATE_MODELS = { 18 | 'django': model_templates.TEMPLATE_MODEL_DJANGO, 19 | 'basemodel': model_templates.TEMPLATE_MODEL_BASEMODEL, 20 | 'softdelete': model_templates.TEMPLATE_MODEL_SOFTDELETEMODEL, 21 | } 22 | 23 | TEMPLATE_ADMINS = { 24 | 'django': admin_templates.TEMPLATE_ADMIN_DJANGO, 25 | 'basemodel': admin_templates.TEMPLATE_ADMIN_BASEMODEL, 26 | 'softdelete': admin_templates.TEMPLATE_ADMIN_SOFTDELETEMODEL, 27 | } 28 | 29 | USER_REMINDER = """ 30 | 31 | `{model_name}` related files created successfully: 32 | 33 | - `{app_name}/models/{model_name_lower}.py` 34 | - `{app_name}/admin/{model_name_lower}.py` 35 | 36 | Please check your models before running `makemigrations` ok? 37 | 38 | """ 39 | 40 | 41 | class Command(BaseCommand): 42 | help = ( 43 | 'Creates models/MODEL.py, admin/MODEL.py for given application' 44 | ) 45 | 46 | MODEL_TYPE_CHOISES = [ 47 | 'django', 48 | 'basemodel', 49 | 'softdelete', 50 | ] 51 | 52 | def create_or_modify_file(self, filename, content, mode='w'): 53 | with open(filename, mode) as f: 54 | f.write(content) 55 | 56 | def add_arguments(self, parser): 57 | parser.add_argument('app_name', nargs=1, type=str, help='Name of your application') 58 | parser.add_argument('model_name', nargs=1, type=str, help='Name of your model') 59 | parser.add_argument( 60 | 'model_type', 61 | nargs='?', 62 | default='django', 63 | choices=self.MODEL_TYPE_CHOISES, 64 | help='Type of your model') 65 | 66 | def handle(self, *args, **options): 67 | app_name = options.pop('app_name')[0] 68 | model_name = options.pop('model_name')[0] 69 | model_type = options.pop('model_type') 70 | 71 | try: 72 | import_module(app_name) 73 | except ImportError: 74 | raise CommandError( 75 | '%s is not exists. Please pass existing application name.' % app_name 76 | ) 77 | 78 | if model_name.lower() in [model.__name__.lower() for model in apps.get_app_config(app_name).get_models()]: 79 | raise CommandError( 80 | '%s model is already exists in %s. Please try non-existing model name.' % (model_name, app_name) 81 | ) 82 | 83 | app_dir = os.path.join(settings.BASE_DIR, 'applications', app_name) 84 | 85 | dash_seperated_file_base_name = '_'.join( 86 | [m for m in re.split('([A-Z][a-z]+)', model_name) if m] 87 | ) 88 | 89 | model_file = os.path.join(app_dir, 'models', '{}.py'.format(dash_seperated_file_base_name.lower())) 90 | model_init_file = os.path.join(app_dir, 'models', '__init__.py') 91 | 92 | admin_file = os.path.join(app_dir, 'admin', '{}.py'.format(dash_seperated_file_base_name.lower())) 93 | admin_init_file = os.path.join(app_dir, 'admin', '__init__.py') 94 | 95 | content_model_file = TEMPLATE_MODELS[model_type].format( 96 | model_name=model_name, 97 | app_name=app_name, 98 | ) 99 | content_init_file = 'from .{} import *\n'.format(dash_seperated_file_base_name.lower()) 100 | 101 | content_admin_file = TEMPLATE_ADMINS[model_type].format( 102 | model_name=model_name, 103 | app_name=app_name, 104 | ) 105 | 106 | self.create_or_modify_file(model_file, content_model_file) 107 | self.stdout.write(self.style.SUCCESS('models/{} created.'.format(os.path.basename(model_file)))) 108 | 109 | self.create_or_modify_file(admin_file, content_admin_file) 110 | self.stdout.write(self.style.SUCCESS('admin/{} created.'.format(os.path.basename(admin_file)))) 111 | 112 | self.create_or_modify_file(model_init_file, content_init_file, 'a') 113 | self.stdout.write(self.style.SUCCESS('{} model added to models/__init__.py'.format(model_name))) 114 | 115 | self.create_or_modify_file(admin_init_file, content_init_file, 'a') 116 | self.stdout.write(self.style.SUCCESS('{} model added to admin/__init__.py'.format(model_name))) 117 | 118 | self.stdout.write(self.style.NOTICE(USER_REMINDER.format( 119 | app_name=app_name, 120 | model_name=model_name, 121 | model_name_lower=dash_seperated_file_base_name.lower(), 122 | ))) 123 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import * 2 | from .admins import * 3 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/admins/__init__.py: -------------------------------------------------------------------------------- 1 | from .basemodel import * 2 | from .django import * 3 | from .softdelete import * 4 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/admins/basemodel.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_ADMIN_BASEMODEL = """from django.contrib import admin 2 | 3 | from baseapp.admin import BaseAdmin 4 | 5 | from ..models import {model_name} 6 | 7 | 8 | __all__ = [ 9 | '{model_name}Admin', 10 | ] 11 | 12 | 13 | class {model_name}Admin(BaseAdmin): 14 | # sticky_list_filter = None 15 | pass 16 | 17 | 18 | admin.site.register({model_name}, {model_name}Admin) 19 | 20 | """ 21 | 22 | 23 | __all__ = [ 24 | 'TEMPLATE_ADMIN_BASEMODEL', 25 | ] 26 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/admins/django.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_ADMIN_DJANGO = """from django.contrib import admin 2 | 3 | from ..models import {model_name} 4 | 5 | 6 | __all__ = [ 7 | '{model_name}Admin', 8 | ] 9 | 10 | 11 | class {model_name}Admin(admin.ModelAdmin): 12 | pass 13 | 14 | 15 | admin.site.register({model_name}, {model_name}Admin) 16 | 17 | """ 18 | 19 | 20 | __all__ = [ 21 | 'TEMPLATE_ADMIN_DJANGO', 22 | ] 23 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/admins/softdelete.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_ADMIN_SOFTDELETEMODEL = """from django.contrib import admin 2 | 3 | from baseapp.admin import BaseAdminWithSoftDelete 4 | 5 | from ..models import {model_name} 6 | 7 | 8 | __all__ = [ 9 | '{model_name}Admin', 10 | ] 11 | 12 | 13 | class {model_name}Admin(BaseAdminWithSoftDelete): 14 | # sticky_list_filter = None 15 | # hide_deleted_at = False 16 | pass 17 | 18 | 19 | admin.site.register({model_name}, {model_name}Admin) 20 | 21 | """ 22 | 23 | 24 | __all__ = [ 25 | 'TEMPLATE_ADMIN_SOFTDELETEMODEL', 26 | ] -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/application/__init__.py: -------------------------------------------------------------------------------- 1 | from .html import * 2 | from .apps import * 3 | from .urls import * 4 | from .views import * 5 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/application/apps.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_APPS = """from django.utils.translation import ugettext_lazy as _ 2 | from django.apps import AppConfig 3 | 4 | class {app_name_title}Config(AppConfig): 5 | name = '{app_name}' 6 | verbose_name = _('{app_name_title}') 7 | verbose_name_plural = _('{app_name_title}') 8 | 9 | """ 10 | 11 | 12 | __all__ = [ 13 | 'TEMPLATE_APPS', 14 | ] -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/application/html.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_HTML = """{{% extends "baseapp/base.html" %}} 2 | {{% load static %}} 3 | {{% load i18n %}} 4 | 5 | {{% block page_title %}}{app_name_title} Application{{% endblock %}} 6 | 7 | {{% block page_body %}} 8 |
9 |
10 |
11 |

Hello from {app_name_title}

12 | 13 | {{% hdbg %}} 14 |
15 |
16 | 17 |
18 | {{% endblock %}} 19 | 20 | """ 21 | 22 | 23 | __all__ = [ 24 | 'TEMPLATE_HTML', 25 | ] -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/application/urls.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_URLS = """from django.conf.urls import url 2 | 3 | from .views import {app_name_title}View 4 | 5 | urlpatterns = [ 6 | url(regex=r'^$', view={app_name_title}View.as_view(), name='{app_name}_index'), 7 | ] 8 | 9 | """ 10 | 11 | 12 | __all__ = [ 13 | 'TEMPLATE_URLS', 14 | ] -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/application/views.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_VIEWS = """from django.views.generic.base import TemplateView 2 | 3 | from baseapp.mixins import HtmlDebugMixin 4 | from baseapp.utils import console 5 | 6 | 7 | console.configure( 8 | source='{app_name}/views.py', 9 | ) 10 | 11 | 12 | class {app_name_title}View(HtmlDebugMixin, TemplateView): 13 | template_name = '{app_name}/index.html' 14 | 15 | def get_context_data(self, **kwargs): 16 | self.hdbg('Hello from hdbg') 17 | kwargs = super().get_context_data(**kwargs) 18 | console.dir(self.request.user) 19 | return kwargs 20 | 21 | """ 22 | 23 | 24 | __all__ = [ 25 | 'TEMPLATE_VIEWS', 26 | ] -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .basemodel import * 2 | from .django import * 3 | from .softdelete import * 4 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/models/basemodel.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_MODEL_BASEMODEL = """from django.utils.translation import ugettext_lazy as _ 2 | from django.db import models 3 | 4 | from baseapp.models import BaseModel 5 | 6 | 7 | __all__ = [ 8 | '{model_name}', 9 | ] 10 | 11 | 12 | class {model_name}(BaseModel): 13 | title = models.CharField( 14 | max_length=255, 15 | verbose_name=_('title'), 16 | ) 17 | 18 | class Meta: 19 | app_label = '{app_name}' 20 | verbose_name = _('{model_name}') 21 | verbose_name_plural = _('{model_name}') 22 | 23 | def __str__(self): 24 | return self.title 25 | 26 | """ 27 | 28 | 29 | __all__ = [ 30 | 'TEMPLATE_MODEL_BASEMODEL', 31 | ] 32 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/models/django.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_MODEL_DJANGO = """from django.utils.translation import ugettext_lazy as _ 2 | from django.db import models 3 | 4 | 5 | __all__ = [ 6 | '{model_name}', 7 | ] 8 | 9 | class {model_name}(models.Model): 10 | created_at = models.DateTimeField( 11 | auto_now_add=True, 12 | verbose_name=_('Created At'), 13 | ) 14 | updated_at = models.DateTimeField( 15 | auto_now=True, 16 | verbose_name=_('Updated At'), 17 | ) 18 | title = models.CharField( 19 | max_length=255, 20 | verbose_name=_('title'), 21 | ) 22 | 23 | class Meta: 24 | app_label = '{app_name}' 25 | verbose_name = _('{model_name}') 26 | verbose_name_plural = _('{model_name}') 27 | 28 | def __str__(self): 29 | return self.title 30 | 31 | """ 32 | 33 | 34 | __all__ = [ 35 | 'TEMPLATE_MODEL_DJANGO', 36 | ] 37 | -------------------------------------------------------------------------------- /applications/baseapp/management/template_structures/models/softdelete.py: -------------------------------------------------------------------------------- 1 | TEMPLATE_MODEL_SOFTDELETEMODEL = """from django.utils.translation import ugettext_lazy as _ 2 | from django.db import models 3 | 4 | from baseapp.models import BaseModelWithSoftDelete 5 | 6 | 7 | __all__ = [ 8 | '{model_name}', 9 | ] 10 | 11 | 12 | class {model_name}(BaseModelWithSoftDelete): 13 | title = models.CharField( 14 | max_length=255, 15 | verbose_name=_('title'), 16 | ) 17 | 18 | class Meta: 19 | app_label = '{app_name}' 20 | verbose_name = _('{model_name}') 21 | verbose_name_plural = _('{model_name}') 22 | 23 | def __str__(self): 24 | return self.title 25 | 26 | """ 27 | 28 | 29 | __all__ = [ 30 | 'TEMPLATE_MODEL_SOFTDELETEMODEL', 31 | ] 32 | -------------------------------------------------------------------------------- /applications/baseapp/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from .locale import * 2 | -------------------------------------------------------------------------------- /applications/baseapp/middlewares/locale.py: -------------------------------------------------------------------------------- 1 | from django.utils.cache import patch_vary_headers 2 | from django.utils import translation 3 | 4 | 5 | __all__ = [ 6 | 'CustomLocaleMiddleware', 7 | ] 8 | 9 | 10 | class CustomLocaleMiddleware(object): 11 | """ 12 | `/en/path/to/page/` sets `request.LANGUAGE_CODE` to `en` otherwise `tr`. 13 | 14 | add this manually to your `MIDDLEWARE` list: 15 | 16 | # settings/base.py 17 | 18 | MIDDLEWARE += [ 19 | 'baseapp.middlewares.CustomLocaleMiddleware', 20 | ] 21 | 22 | You can access it via `request.LANGUAGE_CODE` 23 | 24 | """ 25 | 26 | def __init__(self, get_response): 27 | self.get_response = get_response 28 | 29 | def __call__(self, request): 30 | if request.META['PATH_INFO'].startswith('/en'): 31 | language = 'en' 32 | else: 33 | language = 'tr' 34 | 35 | translation.activate(language) 36 | request.LANGUAGE_CODE = translation.get_language() 37 | response = self.get_response(request) 38 | 39 | patch_vary_headers(response, ('Accept-Language',)) 40 | response['Content-Language'] = translation.get_language() 41 | translation.deactivate() 42 | return response 43 | -------------------------------------------------------------------------------- /applications/baseapp/migrations/0001_create_custom_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-10-02 18:50 3 | from __future__ import unicode_literals 4 | 5 | import baseapp.models.user 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0008_alter_user_username_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), 26 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), 27 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 28 | ('first_name', models.CharField(max_length=255, verbose_name='first name')), 29 | ('middle_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='middle name')), 30 | ('last_name', models.CharField(max_length=255, verbose_name='last name')), 31 | ('avatar', models.FileField(blank=True, null=True, upload_to=baseapp.models.user.save_user_avatar, verbose_name='Profile Image')), 32 | ('is_active', models.BooleanField(default=True, verbose_name='active')), 33 | ('is_staff', models.BooleanField(default=False, verbose_name='staff status')), 34 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 35 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | }, 41 | managers=[ 42 | ('objects', baseapp.models.user.UserManager()), 43 | ], 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /applications/baseapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/migrations/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .html_debug import * -------------------------------------------------------------------------------- /applications/baseapp/mixins/html_debug.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils import translation 3 | 4 | 5 | ___all___ = [ 6 | 'HtmlDebugMixin', 7 | ] 8 | 9 | 10 | class HtmlDebugMixin: 11 | def __init__(self, *args, **kwargs): 12 | self.debug_output = list() 13 | 14 | def hdbg(self, *args): 15 | """ 16 | Works only if DEBUG is True 17 | """ 18 | 19 | if settings.DEBUG and args not in self.debug_output: 20 | self.debug_output.append(args) 21 | 22 | def get_context_data(self, **kwargs): 23 | """ 24 | Works only if DEBUG is True 25 | """ 26 | 27 | kwargs = super().get_context_data(**kwargs) 28 | 29 | custom_template_variables = { 30 | 'IS_DEBUG': settings.DEBUG, 31 | 'LANG': translation.get_language(), 32 | } 33 | kwargs.update(**custom_template_variables) 34 | 35 | if settings.DEBUG: 36 | kwargs.update(hdbg_data=self.debug_output) 37 | return kwargs 38 | -------------------------------------------------------------------------------- /applications/baseapp/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .user import * 3 | -------------------------------------------------------------------------------- /applications/baseapp/models/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.utils import timezone 4 | from django.utils.translation import ugettext_lazy as _ 5 | from django.db import models 6 | 7 | 8 | __all__ = [ 9 | 'BaseModel', 10 | 'BaseModelWithSoftDelete', 11 | ] 12 | 13 | 14 | logger = logging.getLogger('user_logger') 15 | 16 | class BaseModelQuerySet(models.QuerySet): 17 | """ 18 | Common QuerySet for BaseModel and BaseModelWithSoftDelete. 19 | Both querysets have: 20 | 21 | - `.actives()`: Returns `status` = `STATUS_ONLINE` 22 | - `.deleted()`: Returns `status` = `STATUS_DELETED` 23 | - `.offlines()`: Returns `status` = `STATUS_OFFLINE` 24 | - `.drafts()`: Returns `status` = `STATUS_DRAFT` 25 | 26 | methods. 27 | 28 | """ 29 | 30 | def actives(self): 31 | return self.filter( 32 | status=BaseModel.STATUS_ONLINE, 33 | ) 34 | 35 | def deleted(self): 36 | return self.filter( 37 | status=BaseModel.STATUS_DELETED, 38 | ) 39 | 40 | def offlines(self): 41 | return self.filter( 42 | status=BaseModel.STATUS_OFFLINE, 43 | ) 44 | 45 | def drafts(self): 46 | return self.filter( 47 | status=BaseModel.STATUS_DRAFT, 48 | ) 49 | 50 | 51 | class BaseModelWithSoftDeleteQuerySet(BaseModelQuerySet): 52 | """ 53 | Available methods are: 54 | 55 | - `.all()`: Mimics deleted records. Return only if the `deleted_at` value is NULL! 56 | - `.deleted()`: Returns soft deleted objects. 57 | - `.actives()`: Returns `status` = `STATUS_ONLINE` 58 | - `.offlines()`: Returns `status` = `STATUS_OFFLINE` 59 | - `.drafts()`: Returns `status` = `STATUS_DRAFT` 60 | - `.delete()`: Soft deletes give objects. 61 | - `.undelete()`: Recovers (sets `status` to `STATUS_ONLINE`) give objects. 62 | 63 | """ 64 | 65 | def _delete_or_undelete(self, undelete=False): 66 | processed_instances = {} 67 | call_method = 'undelete' if undelete else 'delete' 68 | 69 | for model_instance in self: 70 | _count, model_information = getattr(model_instance, call_method)() 71 | for app_label, row_amount in model_information.items(): 72 | processed_instances.setdefault(app_label, 0) 73 | processed_instances[app_label] = processed_instances[app_label] + row_amount 74 | return (sum(processed_instances.values()), processed_instances) 75 | 76 | def all(self): 77 | return self.filter(deleted_at__isnull=True) 78 | 79 | def actives(self): 80 | return self.all().filter(status=BaseModel.STATUS_ONLINE) 81 | 82 | def offlines(self): 83 | return self.all().filter(status=BaseModel.STATUS_OFFLINE) 84 | 85 | def drafts(self): 86 | return self.all().filter(status=BaseModel.STATUS_DRAFT) 87 | 88 | def delete(self): 89 | return self._delete_or_undelete() 90 | 91 | def undelete(self): 92 | return self._delete_or_undelete(True) 93 | 94 | 95 | class BaseModelWithSoftDeleteManager(models.Manager): 96 | """ 97 | This is a manager for `BaseModelWithSoftDelete` instances. 98 | Do not forget! `.all()` will never return soft-deleted objects! 99 | """ 100 | 101 | def get_queryset(self): 102 | return BaseModelWithSoftDeleteQuerySet(self.model, using=self._db) 103 | 104 | def all(self): 105 | return self.get_queryset().all() 106 | 107 | def deleted(self): 108 | return self.get_queryset().deleted() 109 | 110 | def actives(self): 111 | return self.get_queryset().actives() 112 | 113 | def offlines(self): 114 | return self.get_queryset().offlines() 115 | 116 | def drafts(self): 117 | return self.get_queryset().drafts() 118 | 119 | def delete(self): 120 | return self.get_queryset().delete() 121 | 122 | def undelete(self): 123 | return self.get_queryset().undelete() 124 | 125 | 126 | class BaseModel(models.Model): 127 | """ 128 | Use this model for common functionality 129 | """ 130 | 131 | STATUS_OFFLINE = 0 132 | STATUS_ONLINE = 1 133 | STATUS_DELETED = 2 134 | STATUS_DRAFT = 3 135 | 136 | STATUS_CHOICES = ( 137 | (STATUS_OFFLINE, _('Offline')), 138 | (STATUS_ONLINE, _('Online')), 139 | (STATUS_DELETED, _('Deleted')), 140 | (STATUS_DRAFT, _('Draft')), 141 | ) 142 | 143 | created_at = models.DateTimeField( 144 | auto_now_add=True, 145 | verbose_name=_('Created At'), 146 | ) 147 | updated_at = models.DateTimeField( 148 | auto_now=True, 149 | verbose_name=_('Updated At'), 150 | ) 151 | status = models.IntegerField( 152 | choices=STATUS_CHOICES, 153 | default=STATUS_ONLINE, 154 | verbose_name=_('Status'), 155 | ) 156 | 157 | objects = models.Manager() 158 | objects_bm = BaseModelQuerySet.as_manager() 159 | 160 | class Meta: 161 | abstract = True 162 | 163 | 164 | class BaseModelWithSoftDelete(BaseModel): 165 | deleted_at = models.DateTimeField( 166 | null=True, 167 | blank=True, 168 | verbose_name=_('Deleted At'), 169 | ) 170 | 171 | objects = models.Manager() 172 | objects_bm = BaseModelWithSoftDeleteManager() 173 | 174 | class Meta: 175 | abstract = True 176 | 177 | def delete(self, *args, **kwargs): 178 | return self._delete_or_undelete() 179 | 180 | def undelete(self): 181 | return self._delete_or_undelete(True) 182 | 183 | def _delete_or_undelete(self, undelete=False): 184 | processed_instances = {} 185 | call_method = 'undelete' if undelete else 'delete' 186 | 187 | log_params = { 188 | 'instance': self, 189 | 'label': self._meta.label, 190 | 'pk': self.pk, 191 | } 192 | log_message = '{action} on: "{instance} - pk: {pk}" [{label}]' 193 | 194 | if call_method == 'delete': 195 | models.signals.pre_delete.send(sender=self.__class__, instance=self,) 196 | status_value = self.STATUS_DELETED 197 | deleted_at_value = timezone.now() 198 | log_params.update(action='Soft-delete') 199 | logger.warning(log_message.format(**log_params)) 200 | else: 201 | status_value = self.STATUS_ONLINE 202 | deleted_at_value = None 203 | log_params.update(action='Un-delete') 204 | logger.warning(log_message.format(**log_params)) 205 | 206 | self.status = status_value 207 | self.deleted_at = deleted_at_value 208 | self.save() 209 | 210 | if call_method == 'delete': 211 | models.signals.post_delete.send(sender=self.__class__, instance=self,) 212 | 213 | processed_instances.update({self._meta.label: 1}) 214 | 215 | for related_object in self._meta.related_objects: 216 | if hasattr(related_object, 'on_delete') and getattr(related_object, 'on_delete') == models.CASCADE: 217 | accessor_name = related_object.get_accessor_name() 218 | related_model_instances = getattr(self, accessor_name) 219 | related_model_instance_count = 0 220 | for related_model_instance in related_model_instances.all(): 221 | getattr(related_model_instance, call_method)() 222 | processed_instances.setdefault(related_model_instance._meta.label, related_model_instance_count) 223 | related_model_instance_count += 1 224 | processed_instances.update({related_model_instance._meta.label: related_model_instance_count}) 225 | return (sum(processed_instances.values()), processed_instances) 226 | -------------------------------------------------------------------------------- /applications/baseapp/models/user.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.db import models 3 | from django.contrib.auth.models import ( 4 | AbstractBaseUser, 5 | BaseUserManager, 6 | PermissionsMixin 7 | ) 8 | 9 | from baseapp.utils import save_file as custom_save_file 10 | 11 | 12 | __all__ = [ 13 | 'User', 14 | ] 15 | 16 | 17 | class UserManager(BaseUserManager): 18 | """ 19 | Our custom User model's basic needs. 20 | """ 21 | 22 | use_in_migrations = True 23 | 24 | def create_user(self, email, first_name, last_name, middle_name=None, password=None): 25 | if not email: 26 | raise ValueError(_('Users must have an email address')) 27 | 28 | user_create_fields = { 29 | 'email': email, 30 | 'first_name': first_name, 31 | 'last_name': last_name, 32 | } 33 | 34 | if middle_name: 35 | user_create_fields['middle_name'] = middle_name 36 | 37 | user = self.model(**user_create_fields) 38 | user.set_password(password) 39 | user.save(using=self._db) 40 | return user 41 | 42 | def create_superuser(self, email, first_name, last_name, middle_name=None, password=None): 43 | user = self.create_user(email, first_name, last_name, middle_name, password) 44 | user.is_staff = True 45 | user.is_superuser = True 46 | user.save(using=self._db) 47 | return user 48 | 49 | 50 | def save_user_avatar(instance, filename): 51 | return custom_save_file(instance, filename, upload_to='avatar/') 52 | 53 | 54 | class User(AbstractBaseUser, PermissionsMixin): 55 | USERNAME_FIELD = 'email' 56 | REQUIRED_FIELDS = ['first_name', 'last_name'] 57 | 58 | created_at = models.DateTimeField( 59 | auto_now_add=True, 60 | verbose_name=_('Created At'), 61 | ) 62 | updated_at = models.DateTimeField( 63 | auto_now=True, 64 | verbose_name=_('Updated At'), 65 | ) 66 | email = models.EmailField( 67 | unique=True, 68 | verbose_name=_('email address'), 69 | ) 70 | first_name = models.CharField( 71 | max_length=255, 72 | verbose_name=_('first name'), 73 | ) 74 | middle_name = models.CharField( 75 | max_length=255, 76 | null=True, 77 | blank=True, 78 | verbose_name=_('middle name'), 79 | ) 80 | last_name = models.CharField( 81 | max_length=255, 82 | verbose_name=_('last name'), 83 | ) 84 | avatar = models.FileField( 85 | upload_to=save_user_avatar, 86 | verbose_name=_('Profile Image'), 87 | null=True, 88 | blank=True, 89 | ) 90 | is_active = models.BooleanField( 91 | default=True, 92 | verbose_name=_('active'), 93 | ) 94 | is_staff = models.BooleanField( 95 | default=False, 96 | verbose_name=_('staff status'), 97 | ) 98 | 99 | objects = UserManager() 100 | 101 | class Meta: 102 | app_label = 'baseapp' 103 | verbose_name = _('user') 104 | verbose_name_plural = _('users') 105 | 106 | def __str__(self): 107 | return self.get_full_name() 108 | 109 | def get_short_name(self): 110 | return self.first_name 111 | 112 | def get_full_name(self): 113 | params = { 114 | 'first_name': self.first_name, 115 | 'middle_name': ' ', 116 | 'last_name': self.last_name, 117 | } 118 | if self.middle_name: 119 | params['middle_name'] = ' {middle_name} '.format( 120 | middle_name=self.middle_name) 121 | full_name = '{first_name}{middle_name}{last_name}'.format(**params) 122 | return full_name.strip() 123 | -------------------------------------------------------------------------------- /applications/baseapp/static/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, 3 | BlinkMacSystemFont, 4 | "Segoe UI", 5 | Roboto, 6 | "Helvetica Neue", 7 | Arial, sans-serif !important; 8 | } 9 | 10 | .baseapp-debug { 11 | background: #ddd; 12 | } 13 | 14 | .baseapp-debug pre { 15 | padding: 1rem; 16 | line-height: 1.8em; 17 | overflow-wrap: break-word; 18 | } -------------------------------------------------------------------------------- /applications/baseapp/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/templatetags/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/templatetags/html_debug.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | 3 | from django.conf import settings 4 | from django import template 5 | from django.utils.html import ( 6 | format_html, 7 | escape, 8 | ) 9 | from django.utils.safestring import mark_safe 10 | 11 | register = template.Library() 12 | 13 | 14 | @register.simple_tag(takes_context=True) 15 | def hdbg(context): 16 | """ 17 | Works only if DEBUG is True! 18 | """ 19 | 20 | if not settings.DEBUG: 21 | return '' 22 | 23 | out = [] 24 | hdbg_data = context.get('hdbg_data', []) 25 | for row in hdbg_data: 26 | out.append(escape(pprint.pformat(row))) 27 | return format_html('
{}
', 28 | mark_safe('\n'.join(out))) 29 | -------------------------------------------------------------------------------- /applications/baseapp/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/applications/baseapp/tests/__init__.py -------------------------------------------------------------------------------- /applications/baseapp/tests/base_models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from ..models import BaseModel, BaseModelWithSoftDelete 4 | 5 | class BasicPost(BaseModel): 6 | title = models.CharField( 7 | max_length=255, 8 | ) 9 | 10 | class Meta: 11 | app_label = 'baseapp' 12 | 13 | def __str__(self): 14 | return self.title 15 | 16 | 17 | class Category(BaseModelWithSoftDelete): 18 | title = models.CharField( 19 | max_length=255, 20 | ) 21 | 22 | class Meta: 23 | app_label = 'baseapp' 24 | 25 | def __str__(self): 26 | return self.title 27 | 28 | 29 | class Post(BaseModelWithSoftDelete): 30 | category = models.ForeignKey( 31 | to='Category', 32 | on_delete=models.CASCADE, 33 | related_name='posts', 34 | ) 35 | title = models.CharField( 36 | max_length=255, 37 | ) 38 | 39 | class Meta: 40 | app_label = 'baseapp' 41 | 42 | def __str__(self): 43 | return self.title 44 | 45 | 46 | -------------------------------------------------------------------------------- /applications/baseapp/tests/test_basemodel.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .base_models import BasicPost 4 | 5 | 6 | class BaseModelTestCase(TestCase): 7 | @classmethod 8 | def setUpTestData(cls): 9 | cls.post = BasicPost.objects.create(title='Test Post 1') 10 | cls.post_status_deleted = BasicPost.objects.create(title='Test Post 2', status=BasicPost.STATUS_DELETED) 11 | cls.post_status_offline = BasicPost.objects.create(title='Test Post 3', status=BasicPost.STATUS_OFFLINE) 12 | cls.post_status_draft = BasicPost.objects.create(title='Test Post 4', status=BasicPost.STATUS_DRAFT) 13 | 14 | def test_basemodel_fields(self): 15 | self.assertEqual(self.post.pk, self.post.id) 16 | self.assertEqual(self.post.status, BasicPost.STATUS_ONLINE) 17 | 18 | def test_basemodel_queryset(self): 19 | self.assertQuerysetEqual(BasicPost.objects_bm.all().order_by('id'), [ 20 | '', 21 | '', 22 | '', 23 | '' 24 | ]) 25 | self.assertQuerysetEqual(BasicPost.objects_bm.actives().order_by('id'), ['']) 26 | self.assertQuerysetEqual(BasicPost.objects_bm.offlines(), ['']) 27 | self.assertQuerysetEqual(BasicPost.objects_bm.deleted(), ['']) 28 | self.assertQuerysetEqual(BasicPost.objects_bm.drafts(), ['']) 29 | -------------------------------------------------------------------------------- /applications/baseapp/tests/test_basemodelwithsoftdelete.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .base_models import ( 4 | Category, 5 | Post, 6 | ) 7 | 8 | 9 | class BaseModelWithSoftDeleteTestCase(TestCase): 10 | @classmethod 11 | def setUpTestData(cls): 12 | category = Category.objects.create(title='Python') 13 | cls.category = category 14 | cls.posts = [ 15 | Post.objects.create(category=category, title='Python post 1'), 16 | Post.objects.create(category=category, title='Python post 2'), 17 | ] 18 | 19 | def test_basemodelwithsoftdelete_fields(self): 20 | self.assertEqual(self.category.pk, self.category.id) 21 | self.assertEqual(self.category.status, Post.STATUS_ONLINE) 22 | for post in self.posts: 23 | self.assertEqual(post.status, Post.STATUS_ONLINE) 24 | 25 | def test_basemodelwithsoftdelete_queryset(self): 26 | self.assertQuerysetEqual(self.category.posts.all().order_by('id'), [ 27 | '', 28 | '' 29 | ]) 30 | self.assertQuerysetEqual(Category.objects_bm.actives(), ['']) 31 | self.assertQuerysetEqual(Category.objects_bm.offlines(), []) 32 | self.assertQuerysetEqual(Category.objects_bm.deleted(), []) 33 | self.assertQuerysetEqual(Category.objects_bm.drafts(), []) 34 | 35 | def test_softdelete(self): 36 | deleted_category = self.category.delete() 37 | self.assertEqual(deleted_category, (3, {'baseapp.Category': 1, 'baseapp.Post': 2})) 38 | self.assertQuerysetEqual(Category.objects_bm.deleted(), ['']) 39 | self.assertQuerysetEqual(Category.objects.all(), ['']) 40 | self.assertQuerysetEqual(Post.objects_bm.deleted().order_by('id'), [ 41 | '', 42 | '' 43 | ]) 44 | -------------------------------------------------------------------------------- /applications/baseapp/tests/test_user.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..models import User 4 | 5 | 6 | __all__ = [ 7 | 'CustomUserTestCase', 8 | ] 9 | 10 | 11 | class CustomUserTestCase(TestCase): 12 | 13 | def test_create_user(self): 14 | user = User.objects.create(email='foo@bar.com', 15 | first_name='Uğur', 16 | last_name='Özyılmazel', 17 | password='1234',) 18 | self.assertEqual(user.pk, user.id) 19 | self.assertEqual(user.is_active, True) 20 | self.assertEqual(user.is_staff, False) 21 | self.assertEqual(user.is_superuser, False) 22 | 23 | def test_create_staffuser(self): 24 | user = User.objects.create(email='foo@bar.com', 25 | first_name='Uğur', 26 | last_name='Özyılmazel', 27 | password='1234',) 28 | user.is_staff = True 29 | user.save() 30 | self.assertEqual(user.pk, user.id) 31 | self.assertEqual(user.is_active, True) 32 | self.assertEqual(user.is_staff, True) 33 | self.assertEqual(user.is_superuser, False) 34 | 35 | def test_create_superuser(self): 36 | user = User.objects.create(email='foo@bar.com', 37 | first_name='Uğur', 38 | last_name='Özyılmazel', 39 | password='1234',) 40 | user.is_staff = True 41 | user.is_superuser = True 42 | user.save() 43 | self.assertEqual(user.pk, user.id) 44 | self.assertEqual(user.is_active, True) 45 | self.assertEqual(user.is_staff, True) 46 | self.assertEqual(user.is_superuser, True) 47 | -------------------------------------------------------------------------------- /applications/baseapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from .views import IndexView 3 | 4 | urlpatterns = [ 5 | url(regex=r'^$', view=IndexView.as_view(), name='index'), 6 | ] -------------------------------------------------------------------------------- /applications/baseapp/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .console import * 2 | from .numerify import * 3 | from .urlify import * 4 | from .upload_handler import * 5 | -------------------------------------------------------------------------------- /applications/baseapp/utils/console.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import shutil 3 | 4 | DEBUG = True 5 | 6 | try: 7 | from django.conf import settings 8 | DEBUG = settings.DEBUG 9 | except BaseException: 10 | pass 11 | 12 | TERMINAL_COLUMNS, TERMINAL_LINES = shutil.get_terminal_size() 13 | 14 | 15 | __all__ = [ 16 | 'console', 17 | ] 18 | 19 | 20 | class Console: 21 | """ 22 | 23 | Usage: 24 | 25 | from baseapp.utils import console 26 | 27 | You can set default configuration such as: 28 | 29 | # main configuration example 30 | console.configure( 31 | char='x', 32 | source='console.py', 33 | width=8, # demo purpose 34 | indent=8, # demo purpose 35 | color='white', 36 | ) 37 | 38 | Examples: 39 | 40 | console('Some', 'examples', 'of', 'console', 'usage') 41 | 42 | # change config on the fly 43 | console('Hello', 'World', 'Foo', 'Bar', 'Baz', width=12, indent=8, color='yellow') 44 | 45 | console.configure(color='default') 46 | console(['this', 'is', 'a', 'list'], ('and', 'a', 'tuple',)) 47 | 48 | Available colors: 49 | 50 | - black 51 | - red 52 | - green 53 | - yellow 54 | - blue 55 | - magenta 56 | - cyan 57 | - white 58 | - default 59 | 60 | `console.dir()` inspired from Ruby's `inspection` style. This inspects 61 | most of the attributes of given Object. 62 | 63 | Examples: 64 | 65 | class MyClass: 66 | klass_var1 = 1 67 | klass_var2 = 2 68 | 69 | def __init__(self): 70 | self.name = 'Name' 71 | 72 | def start(self): 73 | return 'method' 74 | 75 | @property 76 | def admin(self): 77 | return True 78 | 79 | @staticmethod 80 | def statik(): 81 | return 'Static' 82 | 83 | @classmethod 84 | def klass_method(cls): 85 | return 'kls' 86 | 87 | mc = MyClass() 88 | 89 | console.dir(MyClass) 90 | console.dir(mc) 91 | 92 | console.dir({}) 93 | 94 | """ 95 | 96 | __colors = dict(black=0, red=1, green=2, yellow=3, blue=4, magenta=5, cyan=6, white=7, default=8) 97 | __color = 'yellow' 98 | __color_reset = '\033[0m' 99 | 100 | __seperator_line = '{source:{char}<{length}}' 101 | __seperator_char = '*' 102 | 103 | def __init__(self, *args, **kwargs): 104 | self.__width = 79 105 | self.__indent = 4 106 | self.__source = __name__ 107 | self.__call__(*args, **kwargs) 108 | 109 | def __call__(self, *args, **kwargs): 110 | self.configure(**kwargs) 111 | self.print(*args, **kwargs) 112 | 113 | def dir(self, *args, **kwargs): 114 | for arg in args: 115 | out = {'arg': args} 116 | source_name = arg.__class__.__name__ 117 | 118 | if source_name != 'type': 119 | source_name = 'instance of {}'.format(source_name) 120 | 121 | if hasattr(arg, '__name__'): 122 | source_name = arg.__name__ 123 | 124 | source = '{} | {}'.format(source_name, type(arg)) 125 | public_attributes = [] 126 | internal_methods = [] 127 | private_methods = [] 128 | 129 | for object_method in dir(arg): 130 | if object_method.startswith('__'): 131 | internal_methods.append(object_method) 132 | elif object_method.startswith('_'): 133 | private_methods.append(object_method) 134 | else: 135 | public_attributes.append(object_method) 136 | 137 | if public_attributes: 138 | out.update(public_attributes=public_attributes) 139 | if internal_methods: 140 | out.update(internal_methods=internal_methods) 141 | if private_methods: 142 | out.update(private_methods=private_methods) 143 | 144 | if hasattr(arg, '__dict__'): 145 | property_list = [] 146 | static_methods = [] 147 | class_methods = [] 148 | public_methods = [] 149 | 150 | for obj_attr, obj_attr_val in arg.__dict__.items(): 151 | _name = type(obj_attr_val).__name__ 152 | 153 | if _name == 'property': 154 | property_list.append(obj_attr) 155 | if obj_attr in public_attributes: 156 | public_attributes.remove(obj_attr) 157 | 158 | if _name == 'staticmethod': 159 | static_methods.append(obj_attr) 160 | if obj_attr in public_attributes: 161 | public_attributes.remove(obj_attr) 162 | 163 | if _name == 'classmethod': 164 | class_methods.append(obj_attr) 165 | if obj_attr in public_attributes: 166 | public_attributes.remove(obj_attr) 167 | 168 | if _name == 'function': 169 | public_methods.append(obj_attr) 170 | if obj_attr in internal_methods: 171 | internal_methods.remove(obj_attr) 172 | if obj_attr in public_attributes: 173 | public_attributes.remove(obj_attr) 174 | 175 | if property_list: 176 | out.update(property_list=property_list) 177 | if static_methods: 178 | out.update(static_methods=static_methods) 179 | if class_methods: 180 | out.update(class_methods=class_methods) 181 | if public_methods: 182 | out.update(public_methods=public_methods) 183 | 184 | if not arg.__dict__.get('__init__', False): 185 | instance_attributes = [] 186 | for instance_attr in list(arg.__dict__.keys()): 187 | instance_attributes.append(instance_attr) 188 | if instance_attr in public_attributes: 189 | public_attributes.remove(instance_attr) 190 | if instance_attr in public_methods: 191 | public_methods.remove(instance_attr) 192 | out.update(instance_attributes=instance_attributes) 193 | 194 | self.print(out, source=source) 195 | 196 | def configure(self, **kwargs): 197 | self.width = kwargs.get('width', self.__width) 198 | self.indent = kwargs.get('indent', self.__indent) 199 | self.color = kwargs.get('color', self.__color) 200 | self.source = kwargs.get('source', self.__source) 201 | self.seperator_line = self.__seperator_line 202 | self.seperator_char = kwargs.get('char', self.__seperator_char) 203 | self.__width = self.width 204 | self.__indent = self.indent 205 | self.__color = self.color 206 | self.__source = self.source 207 | self.__seperator_char = self.seperator_char 208 | 209 | def colorize(self, color): 210 | if self.__colors.get(color, False): 211 | color_code = self.__colors.get(color) 212 | else: 213 | color_code = self.__colors.get(self.__color) 214 | return "\033[3{}m".format(color_code) 215 | 216 | def print(self, *args, **kwargs): 217 | if DEBUG: 218 | self.pp = pprint.PrettyPrinter(indent=self.indent, width=self.width, compact=True) 219 | if args: 220 | self.print_banner(**kwargs) 221 | self.pp.pprint(args) 222 | self.print_banner(source='', end='\n\n') 223 | 224 | def print_banner(self, **kwargs): 225 | pfmt = dict( 226 | source=kwargs.get('source', self.source + ' '), 227 | char=self.seperator_char, 228 | length=TERMINAL_COLUMNS, 229 | ) 230 | end = '\n' 231 | if kwargs.get('end', False): 232 | end = kwargs.get('end') 233 | 234 | print(self.colorize(self.color) + self.seperator_line.format(**pfmt) + self.__color_reset, end=end) 235 | 236 | 237 | if DEBUG: 238 | console = Console() 239 | else: 240 | console = type('Console', (object,), dict()) 241 | 242 | setattr(console, 'configure', lambda *args, **kwargs: '') 243 | setattr(console, 'dir', lambda *args, **kwargs: '') 244 | setattr(console, '__init__', lambda *args, **kwargs: None) 245 | setattr(console, '__call__', lambda *args, **kwargs: '') 246 | -------------------------------------------------------------------------------- /applications/baseapp/utils/numerify.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'numerify', 3 | ] 4 | 5 | 6 | def numerify(input, default=-1): 7 | """ (number or string, default=-1) -> number 8 | 9 | This is good for query string operations. 10 | 11 | >>> numerify(1) 12 | 1 13 | >>> numerify("1") 14 | 1 15 | >>> numerify("1a") 16 | -1 17 | >>> numerify("ab") 18 | -1 19 | >>> numerify("abc", default=44) 20 | 44 21 | """ 22 | 23 | if str(input).isnumeric(): 24 | return int(input) 25 | return default 26 | 27 | 28 | if __name__ == '__main__': 29 | import doctest 30 | doctest.testmod() 31 | -------------------------------------------------------------------------------- /applications/baseapp/utils/upload_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | 4 | from django.utils.text import slugify 5 | from baseapp.utils import urlify 6 | 7 | 8 | __all__ = [ 9 | 'save_file', 10 | ] 11 | 12 | 13 | def save_file(instance, filename, upload_to='upload/%Y/%m/%d/'): 14 | """ 15 | 16 | By default, this saves to : `MEDIA_ROOT/upload/2017/09/06/` 17 | 18 | You can customize this. In your `models.py`: 19 | 20 | from baseapp.utils import save_file as custom_save_file 21 | 22 | def my_custom_uploader(instance, filename): 23 | 24 | # do your stuff 25 | # at the end, call: 26 | 27 | my_custom_upload_path = 'images/%Y/' 28 | return custom_save_file(instance, filename, upload_to=my_custom_upload_path) 29 | 30 | class MyModel(models.Model): 31 | image = models.FileField( 32 | upload_to='my_custom_uploader', 33 | verbose_name=_('Profile Image'), 34 | ) 35 | 36 | """ 37 | 38 | file_basename, file_extension = os.path.splitext(filename) 39 | file_savename = '{safe_basename}{extension}'.format( 40 | safe_basename=slugify(urlify(file_basename)), 41 | extension=file_extension.lower(), 42 | ) 43 | now = datetime.datetime.now() 44 | return '{upload_to}{file_savename}'.format( 45 | upload_to=now.strftime(upload_to), 46 | file_savename=file_savename, 47 | ) 48 | -------------------------------------------------------------------------------- /applications/baseapp/utils/urlify.py: -------------------------------------------------------------------------------- 1 | LETTER_TRANSFORM_MAP = { 2 | 'tr': { 3 | 'ç': 'c', 4 | 'Ç': 'c', 5 | 'ğ': 'g', 6 | 'Ğ': 'g', 7 | 'ı': 'i', 8 | 'I': 'i', 9 | 'İ': 'i', 10 | 'ö': 'o', 11 | 'Ö': 'o', 12 | 'ş': 's', 13 | 'Ş': 's', 14 | 'ü': 'u', 15 | 'Ü': 'u', 16 | }, 17 | } 18 | 19 | 20 | __all__ = [ 21 | 'urlify', 22 | ] 23 | 24 | 25 | def urlify(value, language='tr'): 26 | """ 27 | This is a pre-processor for django's slugify function. 28 | 29 | Example usage: 30 | 31 | from django.utils.text import slugify 32 | 33 | corrected_text = slugify(urlify('Merhaba Dünya!')) 34 | 35 | >>> urlify('Merhaba Dünya') 36 | 'merhaba dunya' 37 | 38 | >>> urlify('Uğur Özyılmazel') 39 | 'ugur ozyilmazel' 40 | 41 | >>> urlify('ç ğ ü Ç Ğ Ü ı İ') 42 | 'c g u c g u i i' 43 | 44 | """ 45 | 46 | return ''.join(map(lambda char: LETTER_TRANSFORM_MAP[language].get(char, char), iter(value))).lower() 47 | 48 | 49 | if __name__ == '__main__': 50 | import doctest 51 | doctest.testmod() 52 | -------------------------------------------------------------------------------- /applications/baseapp/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.base import TemplateView 2 | from django.utils.text import slugify 3 | 4 | from .mixins import HtmlDebugMixin 5 | 6 | from baseapp.utils import ( 7 | console, 8 | numerify, 9 | urlify, 10 | ) 11 | 12 | console.configure( 13 | source='baseapp/views.py', 14 | ) 15 | 16 | class IndexView(HtmlDebugMixin, TemplateView): 17 | template_name = 'baseapp/index.html' 18 | 19 | def get_context_data(self, **kwargs): 20 | self.hdbg('This', 'is', 'an', 'example', 'of') 21 | self.hdbg('self.hdbg', 'usage') 22 | self.hdbg(self.request.__dict__) 23 | self.hdbg(slugify(urlify('Merhaba Dünya! Ben Uğur Özyılmazel'))) 24 | kwargs = super().get_context_data(**kwargs) 25 | 26 | query_string_p = numerify(self.request.GET.get('p')) 27 | console(query_string_p, type(query_string_p),) 28 | console.dir(self.request.user) 29 | return kwargs 30 | -------------------------------------------------------------------------------- /applications/baseapp/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .image_file import * -------------------------------------------------------------------------------- /applications/baseapp/widgets/image_file.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.contrib.admin.widgets import AdminFileWidget 3 | from django.conf import settings 4 | 5 | from PIL import Image 6 | 7 | 8 | __all__ = [ 9 | 'AdminImageFileWidget', 10 | ] 11 | 12 | 13 | def is_image(file): 14 | try: 15 | with Image.open(file) as im: 16 | return im.size 17 | except IOError: 18 | return False 19 | 20 | 21 | class AdminImageFileWidget(AdminFileWidget): 22 | """ 23 | Example usage in: `admin.py` 24 | 25 | from baseapp.widgets import AdminImageFileWidget 26 | from django.db import models 27 | 28 | class MyModelAdmin(admin.ModelAdmin): 29 | formfield_overrides = { 30 | models.FileField: {'widget': AdminImageFileWidget}, 31 | } 32 | 33 | """ 34 | 35 | def render(self, name, value, attrs=None): 36 | widget = super().render(name, value, attrs) 37 | if value: 38 | possible_image = is_image(value) 39 | if possible_image: 40 | widget = '' \ 41 | '
' \ 42 | '' \ 43 | '

{dimensions}: {width} x {height}

' \ 44 | '
' \ 45 | '{widget}' \ 46 | ''.format( 47 | object_name='{}-preview'.format(name), 48 | image_url='{}{}'.format(settings.MEDIA_URL, value), 49 | dimensions=_('Dimensions'), 50 | width=possible_image[0], 51 | height=possible_image[1], 52 | widget=widget, 53 | ) 54 | return widget 55 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/config/__init__.py -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | SECRET_KEY = os.environ.get('DJANGO_SECRET') 6 | 7 | sys.path.append(os.path.join(BASE_DIR, 'applications')) 8 | 9 | # base apps 10 | INSTALLED_APPS = [ 11 | 'django.contrib.admin', 12 | 'django.contrib.auth', 13 | 'django.contrib.contenttypes', 14 | 'django.contrib.sessions', 15 | 'django.contrib.messages', 16 | 'django.contrib.staticfiles', 17 | 'baseapp', 18 | ] 19 | 20 | # base middlewares 21 | MIDDLEWARE = [ 22 | 'django.middleware.security.SecurityMiddleware', 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.middleware.csrf.CsrfViewMiddleware', 26 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 27 | 'django.contrib.messages.middleware.MessageMiddleware', 28 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 29 | ] 30 | 31 | ROOT_URLCONF = 'config.urls' 32 | 33 | TEMPLATES = [ 34 | { 35 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 36 | 'DIRS': [ 37 | os.path.join(BASE_DIR, 'templates'), 38 | ], 39 | 'APP_DIRS': True, 40 | 'OPTIONS': { 41 | 'builtins': [ 42 | 'baseapp.templatetags.html_debug', 43 | ], 44 | 'context_processors': [ 45 | 'django.template.context_processors.debug', 46 | 'django.template.context_processors.request', 47 | 'django.contrib.auth.context_processors.auth', 48 | 'django.contrib.messages.context_processors.messages', 49 | ], 50 | }, 51 | }, 52 | ] 53 | 54 | WSGI_APPLICATION = 'config.wsgi.application' 55 | 56 | 57 | LANGUAGE_CODE = 'tr' 58 | TIME_ZONE = 'Europe/Istanbul' 59 | USE_I18N = True 60 | USE_L10N = True 61 | USE_TZ = True 62 | 63 | LOCALE_PATHS = ( 64 | os.path.join(BASE_DIR, 'locale'), 65 | ) 66 | 67 | STATIC_URL = '/static/' 68 | MEDIA_URL = '/media/' 69 | 70 | AUTH_USER_MODEL = 'baseapp.User' 71 | -------------------------------------------------------------------------------- /config/settings/development.example.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | from baseapp.libs.log_helpers import ( 4 | CustomWerkzeugLogFormatter, 5 | CustomSqlLogFormatter, 6 | werkzueg_filter_extenstions_callback, 7 | ) 8 | 9 | DEBUG = True 10 | ALLOWED_HOSTS = [] 11 | INTERNAL_IPS = ['127.0.0.1'] 12 | 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', 16 | 'NAME': os.path.join(BASE_DIR, 'db', 'development.sqlite3'), 17 | } 18 | } 19 | 20 | STATICFILES_DIRS = ( 21 | os.path.join(BASE_DIR, 'static'), 22 | ) 23 | 24 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 25 | 26 | CUSTOM_LOGGER_OPTIONS = { 27 | 'hide_these_extensions': ['css', 'js', 'png', 'jpg', 'svg', 'gif', 'woff'], 28 | } 29 | 30 | LOGGING = { 31 | 'version': 1, 32 | 'disable_existing_loggers': True, 33 | 'filters': { 34 | 'werkzueg_filter_extensions': { 35 | '()': 'django.utils.log.CallbackFilter', 36 | 'callback': werkzueg_filter_extenstions_callback, 37 | }, 38 | }, 39 | 'formatters': { 40 | 'custom_sql_query': { 41 | '()': CustomSqlLogFormatter, 42 | 'format': '%(levelname)s |\n%(sql)s\n\ntook: %(duration)f mseconds\n\n', 43 | }, 44 | 'custom_werkzeug_log_formatter': { 45 | '()': CustomWerkzeugLogFormatter, 46 | 'format': '%(levelname)s | %(message)s', 47 | }, 48 | }, 49 | 'handlers': { 50 | 'console_sql': { 51 | 'level': 'DEBUG', 52 | 'class': 'logging.StreamHandler', 53 | 'formatter': 'custom_sql_query', 54 | }, 55 | 'console_custom': { 56 | 'level': 'DEBUG', 57 | 'filters': ['werkzueg_filter_extensions'], 58 | 'class': 'logging.StreamHandler', 59 | 'formatter': 'custom_werkzeug_log_formatter', 60 | } 61 | }, 62 | 'loggers': { 63 | 'werkzeug': { 64 | 'handlers': ['console_custom'], 65 | 'level': 'DEBUG', 66 | 'propagate': True, 67 | }, 68 | 'user_logger': { 69 | 'handlers': ['console_custom'], 70 | 'level': 'DEBUG', 71 | }, 72 | # enable this block if you want to see SQL queries :) 73 | # 'django.db.backends': { 74 | # 'handlers': ['console_sql'], 75 | # 'level': 'DEBUG', 76 | # }, 77 | } 78 | } 79 | 80 | # middlewares for development purposes only 81 | MIDDLEWARE += [ 82 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 83 | ] 84 | 85 | # apps for development purposes only 86 | INSTALLED_APPS += [ 87 | 'django_extensions', 88 | 'debug_toolbar', 89 | ] 90 | -------------------------------------------------------------------------------- /config/settings/heroku.py: -------------------------------------------------------------------------------- 1 | import dj_database_url 2 | 3 | from .base import * 4 | 5 | db_from_env = dj_database_url.config(conn_max_age=500) 6 | 7 | DEBUG = False 8 | ALLOWED_HOSTS = [ 9 | # heroku domain here! 'XXXXX.herokuapp.com', 10 | ] 11 | DATABASES = { 12 | 'default': db_from_env, 13 | } 14 | 15 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 16 | 17 | AUTH_PASSWORD_VALIDATORS = [ 18 | { 19 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 20 | }, 21 | { 22 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 23 | }, 24 | { 25 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 26 | }, 27 | { 28 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 29 | }, 30 | ] 31 | 32 | STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' 33 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 34 | 35 | STATICFILES_DIRS = ( 36 | os.path.join(BASE_DIR, 'static'), 37 | ) 38 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 39 | 40 | INSTALLED_APPS.insert(INSTALLED_APPS.index('django.contrib.staticfiles'), 41 | 'whitenoise.runserver_nostatic') 42 | 43 | MIDDLEWARE.insert(MIDDLEWARE.index('django.middleware.security.SecurityMiddleware') + 1, 44 | 'whitenoise.middleware.WhiteNoiseMiddleware') 45 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | SECRET_KEY = 'fake-key' 4 | 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'django.db.backends.sqlite3', 8 | 'NAME': ":memory:", 9 | } 10 | } 11 | 12 | PASSWORD_HASHERS = ( 13 | 'django.contrib.auth.hashers.MD5PasswordHasher', 14 | ) 15 | 16 | MIGRATION_MODULES = { 17 | 'baseapp': None, 18 | } -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import ( 3 | url, 4 | static, 5 | include, 6 | ) 7 | from django.contrib import admin 8 | 9 | urlpatterns = [ 10 | url(r'^admin/', admin.site.urls), 11 | url(r'^__baseapp__/', include('baseapp.urls', namespace='baseapp')), 12 | ] 13 | 14 | if settings.DEBUG: 15 | import debug_toolbar 16 | urlpatterns += [ 17 | url(r'^__debug__/', include(debug_toolbar.urls)), 18 | ] + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 19 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 6 | 'config.settings.{}'.format(os.environ.get('DJANGO_ENV'))) 7 | 8 | application = get_wsgi_application() 9 | -------------------------------------------------------------------------------- /db/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/db/.gitkeep -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Created by Uğur Özyılmazel on 2017-09-28. 4 | # bash <(curl -fsSL https://raw.githubusercontent.com/vigo/django-project-template/master/install.sh) 5 | 6 | set -e 7 | set -o pipefail 8 | 9 | AVAILABLE_OPTIONS=("Django 1.11.4" "Cancel and quit") 10 | 11 | echo "Django Project Template Installer" 12 | PS3="Select option:" 13 | select i in "${AVAILABLE_OPTIONS[@]}" 14 | do 15 | case $i in 16 | "Django 1.11.4") 17 | PACKAGE="django-1.11.4" 18 | break 19 | ;; 20 | "Cancel and quit") 21 | echo "Canceled..." 22 | exit 1 23 | ;; 24 | esac 25 | done 26 | 27 | echo "What is your project name?" 28 | read PROJECT_NAME 29 | 30 | if [[ ! $PROJECT_NAME ]]; then 31 | echo "Canceled..." 32 | exit 1 33 | fi 34 | 35 | PACKAGE_URL="https://github.com/vigo/django-project-template/archive/${PACKAGE}.zip" 36 | 37 | curl -L "${PACKAGE_URL}" > template.zip && 38 | unzip template.zip && 39 | mv "django-project-template-${PACKAGE}" "${PROJECT_NAME}" && 40 | rm template.zip && 41 | cd "${PROJECT_NAME}" && 42 | cp config/settings/development.example.py config/settings/development.py && 43 | echo 44 | echo 45 | echo "Installation completed..." 46 | echo "Now, create your virtual environment and run:" 47 | echo "cd ${PROJECT_NAME}/" 48 | echo "pip install -r requirements/development.pip" 49 | -------------------------------------------------------------------------------- /locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /locale/tr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Turkish language support for our Django application 2 | # Copyright (C) 2017 Uğur "vigo" Özyılmazel 3 | # This file is distributed under the same license as the Django package. 4 | # Uğur Özyılmazel , 2017. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-base-project\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-09-20 15:59+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Uğur Özyılmazel \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: tr\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: applications/baseapp/admin/base.py:34 22 | msgid "1 record was" 23 | msgstr "1 kayıt" 24 | 25 | #: applications/baseapp/admin/base.py:36 26 | #, python-format 27 | msgid "%(number_of_rows)s records were" 28 | msgstr "%(number_of_rows)s kayıt" 29 | 30 | #: applications/baseapp/admin/base.py:37 31 | #, python-format 32 | msgid "%(message_bit)s successfully marked as active" 33 | msgstr "%(message_bit)s aktif olarak işaretlendi." 34 | 35 | #: applications/baseapp/admin/base.py:58 36 | #, python-format 37 | msgid "Recover selected %(verbose_name_plural)s" 38 | msgstr "Seçili %(verbose_name_plural)s nesnelerini aktif hale getir" 39 | 40 | #: applications/baseapp/admin/user.py:21 applications/baseapp/admin/user.py:52 41 | msgid "Password" 42 | msgstr "" 43 | 44 | #: applications/baseapp/admin/user.py:23 45 | msgid "" 46 | "Raw passwords are not stored, so there is no way to see this user's " 47 | "password, but you can change the password using this form." 49 | msgstr "" 50 | 51 | #: applications/baseapp/admin/user.py:41 applications/baseapp/admin/user.py:68 52 | #: applications/baseapp/models/user.py:81 53 | msgid "first name" 54 | msgstr "" 55 | 56 | #: applications/baseapp/admin/user.py:42 applications/baseapp/admin/user.py:69 57 | #: applications/baseapp/models/user.py:91 58 | msgid "last name" 59 | msgstr "" 60 | 61 | #: applications/baseapp/admin/user.py:56 62 | msgid "Password confirmation" 63 | msgstr "" 64 | 65 | #: applications/baseapp/admin/user.py:76 66 | msgid "Passwords don't match" 67 | msgstr "" 68 | 69 | #: applications/baseapp/admin/user.py:97 70 | msgid "User information" 71 | msgstr "Kullanıcı Bilgileri" 72 | 73 | #: applications/baseapp/admin/user.py:105 74 | msgid "Permissions" 75 | msgstr "" 76 | 77 | #: applications/baseapp/admin/user.py:137 78 | #: applications/baseapp/models/user.py:95 79 | msgid "Profile Image" 80 | msgstr "Profil Fotoğrafı" 81 | 82 | #: applications/baseapp/models/base.py:26 83 | msgid "Offline" 84 | msgstr "Kapalı" 85 | 86 | #: applications/baseapp/models/base.py:27 87 | msgid "Online" 88 | msgstr "Yayında" 89 | 90 | #: applications/baseapp/models/base.py:28 91 | msgid "Deleted" 92 | msgstr "Silinmiş" 93 | 94 | #: applications/baseapp/models/base.py:29 95 | msgid "Draft" 96 | msgstr "Taslak" 97 | 98 | #: applications/baseapp/models/base.py:34 99 | msgid "Created At" 100 | msgstr "Oluşturma Tarihi" 101 | 102 | #: applications/baseapp/models/base.py:38 103 | msgid "Updated At" 104 | msgstr "Güncelleme Tarihi" 105 | 106 | #: applications/baseapp/models/base.py:43 107 | msgid "Status" 108 | msgstr "Durum" 109 | 110 | #: applications/baseapp/models/base.py:76 111 | msgid "Deleted At" 112 | msgstr "Silinme Tarihi" 113 | 114 | #: applications/baseapp/models/user.py:32 115 | msgid "Users must have an email address" 116 | msgstr "" 117 | 118 | #: applications/baseapp/models/user.py:77 119 | msgid "email address" 120 | msgstr "" 121 | 122 | #: applications/baseapp/models/user.py:87 123 | msgid "middle name" 124 | msgstr "göbek adı" 125 | 126 | #: applications/baseapp/models/user.py:99 127 | msgid "active" 128 | msgstr "" 129 | 130 | #: applications/baseapp/models/user.py:103 131 | msgid "staff status" 132 | msgstr "" 133 | 134 | #: applications/baseapp/models/user.py:110 135 | msgid "user" 136 | msgstr "" 137 | 138 | #: applications/baseapp/models/user.py:111 139 | msgid "users" 140 | msgstr "" 141 | 142 | #: applications/baseapp/widgets/image_file.py:46 143 | msgid "Dimensions" 144 | msgstr "Boyutları" 145 | 146 | #: templates/baseapp/index.html:8 147 | msgid "Welcome to Django" 148 | msgstr "" 149 | 150 | #: templates/baseapp/index.html:9 151 | msgid "It worked!" 152 | msgstr "" 153 | 154 | #: templates/baseapp/index.html:10 155 | msgid "Template variables" 156 | msgstr "" 157 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | if not os.environ.get('DJANGO_ENV', False): 7 | raise EnvironmentError('Please define DJANGO_ENV environment variable') 8 | 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 10 | 'config.settings.{}'.format(os.environ.get('DJANGO_ENV'))) 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError: 14 | try: 15 | import django 16 | except ImportError: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) 22 | raise 23 | execute_from_command_line(sys.argv) 24 | -------------------------------------------------------------------------------- /media/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/media/.gitkeep -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/heroku.pip -------------------------------------------------------------------------------- /requirements/base.pip: -------------------------------------------------------------------------------- 1 | Django==1.11.4 2 | Pillow==4.2.1 -------------------------------------------------------------------------------- /requirements/development.pip: -------------------------------------------------------------------------------- 1 | -r base.pip 2 | ipython==6.1.0 3 | django-extensions==1.9.0 4 | Werkzeug==0.12.2 5 | django-debug-toolbar==1.8 -------------------------------------------------------------------------------- /requirements/heroku.pip: -------------------------------------------------------------------------------- 1 | -r base.pip 2 | gunicorn==19.7.1 3 | psycopg2==2.7.3 4 | dj-database-url==0.4.2 5 | whitenoise==3.3.0 -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.2 -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigo/django-project-template/a0458c45934356ab8b33969fdcd4bb6f41a19548/static/.gitkeep -------------------------------------------------------------------------------- /templates/baseapp/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load i18n %} 3 | 4 | 5 | 6 | 7 | 8 | {% block page_title %}Baseapp{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% block page_body %}{% endblock %} 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/baseapp/index.html: -------------------------------------------------------------------------------- 1 | {% extends "baseapp/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block page_body %} 5 |
6 |
7 |
8 |

{% trans "Welcome to Django" %}

9 |

{% trans "It worked!" %}

10 |

{% trans "Template variables" %}

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
{% templatetag openvariable %} IS_DEBUG {% templatetag closevariable %}{{ IS_DEBUG }}
{% templatetag openvariable %} LANG {% templatetag closevariable %}{{ LANG }}
22 | 23 | {% hdbg %} 24 |
25 |
26 | 27 |
28 | {% endblock %} 29 | 30 | 31 | --------------------------------------------------------------------------------