├── .gitignore ├── README.md ├── api ├── Makefile ├── django_blog │ ├── __init__.py │ ├── apps │ │ ├── __init__.py │ │ ├── account │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── forms.py │ │ │ ├── managers.py │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20190902_1644.py │ │ │ │ └── __init__.py │ │ │ └── models.py │ │ ├── blog │ │ │ ├── .gitignore │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ └── __init__.py │ │ │ ├── models │ │ │ │ ├── __init__.py │ │ │ │ └── post.py │ │ │ ├── rest_api │ │ │ │ ├── __init__.py │ │ │ │ ├── serializers │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── post.py │ │ │ │ ├── urls.py │ │ │ │ └── views │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── blog_views.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── test_models.py │ │ │ │ └── tests.py │ │ └── common │ │ │ ├── __init__.py │ │ │ ├── apps.py │ │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ ├── generate_secretkey.py │ │ │ │ └── startapp.py │ │ │ └── models │ │ │ ├── __init__.py │ │ │ └── core.py │ ├── settings │ │ ├── __init__.py │ │ ├── contrib.py │ │ ├── django.py │ │ ├── django_blog.py │ │ └── environment.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── pylama.ini ├── pyproject.toml ├── pytest.ini └── requirements │ ├── common.in │ ├── common.txt │ ├── dev.in │ └── dev.txt ├── build ├── Dockerfile.api ├── ci │ └── circle.yml ├── docker-compose-api.yml ├── docker-compose-dev.yml └── docker-entrypoint-api.sh └── circle.yml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim,osx,emacs,linux,macos,django,python,windows,pycharm,virtualenv,sublimetext,visualstudiocode 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | media 12 | 13 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 14 | # in your Git repository. Update and uncomment the following line accordingly. 15 | # /staticfiles/ 16 | 17 | ### Emacs ### 18 | # -*- mode: gitignore; -*- 19 | *~ 20 | \#*\# 21 | /.emacs.desktop 22 | /.emacs.desktop.lock 23 | *.elc 24 | auto-save-list 25 | tramp 26 | .\#* 27 | 28 | # Org-mode 29 | .org-id-locations 30 | *_archive 31 | 32 | # flymake-mode 33 | *_flymake.* 34 | 35 | # eshell files 36 | /eshell/history 37 | /eshell/lastdir 38 | 39 | # elpa packages 40 | /elpa/ 41 | 42 | # reftex files 43 | *.rel 44 | 45 | # AUCTeX auto folder 46 | /auto/ 47 | 48 | # cask packages 49 | .cask/ 50 | dist/ 51 | 52 | # Flycheck 53 | flycheck_*.el 54 | 55 | # server auth directory 56 | /server/ 57 | 58 | # projectiles files 59 | .projectile 60 | 61 | # directory configuration 62 | .dir-locals.el 63 | 64 | ### Linux ### 65 | 66 | # temporary files which can be created if a process still has a handle open of a deleted file 67 | .fuse_hidden* 68 | 69 | # KDE directory preferences 70 | .directory 71 | 72 | # Linux trash folder which might appear on any partition or disk 73 | .Trash-* 74 | 75 | # .nfs files are created when an open file is removed but is still being accessed 76 | .nfs* 77 | 78 | ### macOS ### 79 | # General 80 | .DS_Store 81 | .AppleDouble 82 | .LSOverride 83 | 84 | # Icon must end with two \r 85 | Icon 86 | 87 | # Thumbnails 88 | ._* 89 | 90 | # Files that might appear in the root of a volume 91 | .DocumentRevisions-V100 92 | .fseventsd 93 | .Spotlight-V100 94 | .TemporaryItems 95 | .Trashes 96 | .VolumeIcon.icns 97 | .com.apple.timemachine.donotpresent 98 | 99 | # Directories potentially created on remote AFP share 100 | .AppleDB 101 | .AppleDesktop 102 | Network Trash Folder 103 | Temporary Items 104 | .apdisk 105 | 106 | ### OSX ### 107 | # General 108 | 109 | # Icon must end with two \r 110 | 111 | # Thumbnails 112 | 113 | # Files that might appear in the root of a volume 114 | 115 | # Directories potentially created on remote AFP share 116 | 117 | ### PyCharm ### 118 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 119 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 120 | 121 | # User-specific stuff 122 | .idea/**/workspace.xml 123 | .idea/**/tasks.xml 124 | .idea/**/usage.statistics.xml 125 | .idea/**/dictionaries 126 | .idea/**/shelf 127 | 128 | # Generated files 129 | .idea/**/contentModel.xml 130 | 131 | # Sensitive or high-churn files 132 | .idea/**/dataSources/ 133 | .idea/**/dataSources.ids 134 | .idea/**/dataSources.local.xml 135 | .idea/**/sqlDataSources.xml 136 | .idea/**/dynamic.xml 137 | .idea/**/uiDesigner.xml 138 | .idea/**/dbnavigator.xml 139 | 140 | # Gradle 141 | .idea/**/gradle.xml 142 | .idea/**/libraries 143 | 144 | # Gradle and Maven with auto-import 145 | # When using Gradle or Maven with auto-import, you should exclude module files, 146 | # since they will be recreated, and may cause churn. Uncomment if using 147 | # auto-import. 148 | # .idea/modules.xml 149 | # .idea/*.iml 150 | # .idea/modules 151 | 152 | # CMake 153 | cmake-build-*/ 154 | 155 | # Mongo Explorer plugin 156 | .idea/**/mongoSettings.xml 157 | 158 | # File-based project format 159 | *.iws 160 | 161 | # IntelliJ 162 | out/ 163 | 164 | # mpeltonen/sbt-idea plugin 165 | .idea_modules/ 166 | 167 | # JIRA plugin 168 | atlassian-ide-plugin.xml 169 | 170 | # Cursive Clojure plugin 171 | .idea/replstate.xml 172 | 173 | # Crashlytics plugin (for Android Studio and IntelliJ) 174 | com_crashlytics_export_strings.xml 175 | crashlytics.properties 176 | crashlytics-build.properties 177 | fabric.properties 178 | 179 | # Editor-based Rest Client 180 | .idea/httpRequests 181 | 182 | ### PyCharm Patch ### 183 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 184 | 185 | # *.iml 186 | # modules.xml 187 | # .idea/misc.xml 188 | # *.ipr 189 | 190 | # Sonarlint plugin 191 | .idea/sonarlint 192 | 193 | ### Python ### 194 | # Byte-compiled / optimized / DLL files 195 | *.py[cod] 196 | *$py.class 197 | 198 | # C extensions 199 | *.so 200 | 201 | # Distribution / packaging 202 | .Python 203 | develop-eggs/ 204 | downloads/ 205 | eggs/ 206 | .eggs/ 207 | lib/ 208 | lib64/ 209 | parts/ 210 | sdist/ 211 | var/ 212 | wheels/ 213 | *.egg-info/ 214 | .installed.cfg 215 | *.egg 216 | MANIFEST 217 | 218 | # PyInstaller 219 | # Usually these files are written by a python script from a template 220 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 221 | *.manifest 222 | *.spec 223 | 224 | # Installer logs 225 | pip-log.txt 226 | pip-delete-this-directory.txt 227 | 228 | # Unit test / coverage reports 229 | htmlcov/ 230 | .tox/ 231 | .coverage 232 | .coverage.* 233 | .cache 234 | nosetests.xml 235 | coverage.xml 236 | *.cover 237 | .hypothesis/ 238 | .pytest_cache/ 239 | 240 | # Translations 241 | *.mo 242 | 243 | # Django stuff: 244 | 245 | # Flask stuff: 246 | instance/ 247 | .webassets-cache 248 | 249 | # Scrapy stuff: 250 | .scrapy 251 | 252 | # Sphinx documentation 253 | docs/_build/ 254 | 255 | # PyBuilder 256 | target/ 257 | 258 | # Jupyter Notebook 259 | .ipynb_checkpoints 260 | 261 | # IPython 262 | profile_default/ 263 | ipython_config.py 264 | 265 | # pyenv 266 | .python-version 267 | 268 | # celery beat schedule file 269 | celerybeat-schedule 270 | 271 | # SageMath parsed files 272 | *.sage.py 273 | 274 | # Environments 275 | .env 276 | .venv 277 | env/ 278 | venv/ 279 | ENV/ 280 | env.bak/ 281 | venv.bak/ 282 | 283 | # Spyder project settings 284 | .spyderproject 285 | .spyproject 286 | 287 | # Rope project settings 288 | .ropeproject 289 | 290 | # mkdocs documentation 291 | /site 292 | 293 | # mypy 294 | .mypy_cache/ 295 | .dmypy.json 296 | dmypy.json 297 | 298 | ### Python Patch ### 299 | .venv/ 300 | 301 | ### Python.VirtualEnv Stack ### 302 | # Virtualenv 303 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 304 | [Bb]in 305 | [Ii]nclude 306 | [Ll]ib 307 | [Ll]ib64 308 | [Ll]ocal 309 | [Ss]cripts 310 | pyvenv.cfg 311 | pip-selfcheck.json 312 | 313 | ### SublimeText ### 314 | # Cache files for Sublime Text 315 | *.tmlanguage.cache 316 | *.tmPreferences.cache 317 | *.stTheme.cache 318 | 319 | # Workspace files are user-specific 320 | *.sublime-workspace 321 | 322 | # Project files should be checked into the repository, unless a significant 323 | # proportion of contributors will probably not be using Sublime Text 324 | # *.sublime-project 325 | 326 | # SFTP configuration file 327 | sftp-config.json 328 | 329 | # Package control specific files 330 | Package Control.last-run 331 | Package Control.ca-list 332 | Package Control.ca-bundle 333 | Package Control.system-ca-bundle 334 | Package Control.cache/ 335 | Package Control.ca-certs/ 336 | Package Control.merged-ca-bundle 337 | Package Control.user-ca-bundle 338 | oscrypto-ca-bundle.crt 339 | bh_unicode_properties.cache 340 | 341 | # Sublime-github package stores a github token in this file 342 | # https://packagecontrol.io/packages/sublime-github 343 | GitHub.sublime-settings 344 | 345 | ### Vim ### 346 | # Swap 347 | [._]*.s[a-v][a-z] 348 | [._]*.sw[a-p] 349 | [._]s[a-rt-v][a-z] 350 | [._]ss[a-gi-z] 351 | [._]sw[a-p] 352 | 353 | # Session 354 | Session.vim 355 | 356 | # Temporary 357 | .netrwhist 358 | # Auto-generated tag files 359 | tags 360 | # Persistent undo 361 | [._]*.un~ 362 | 363 | ### VirtualEnv ### 364 | # Virtualenv 365 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 366 | 367 | ### VisualStudioCode ### 368 | .vscode/* 369 | !.vscode/settings.json 370 | !.vscode/tasks.json 371 | !.vscode/launch.json 372 | !.vscode/extensions.json 373 | 374 | ### Windows ### 375 | # Windows thumbnail cache files 376 | Thumbs.db 377 | ehthumbs.db 378 | ehthumbs_vista.db 379 | 380 | # Dump file 381 | *.stackdump 382 | 383 | # Folder config file 384 | [Dd]esktop.ini 385 | 386 | # Recycle Bin used on file shares 387 | $RECYCLE.BIN/ 388 | 389 | # Windows Installer files 390 | *.cab 391 | *.msi 392 | *.msix 393 | *.msm 394 | *.msp 395 | 396 | # Windows shortcuts 397 | *.lnk 398 | 399 | 400 | # End of https://www.gitignore.io/api/vim,osx,emacs,linux,macos,django,python,windows,pycharm,virtualenv,sublimetext,visualstudiocode 401 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django_blog 2 | 3 | `api` — backend application 4 | 5 | `build` — build scripts, docker/docker-compose 6 | 7 | ### How to run 8 | 9 | #### Development mode 10 | 11 | `docker-compose -f build/docker-compose-dev.yml up` 12 | 13 | will run only services needed for development like PostgreSQL, Redis etc 14 | 15 | `docker-compose -f build/docker-compose-api.yml up` 16 | 17 | will run all needed services and API (backend application) -------------------------------------------------------------------------------- /api/Makefile: -------------------------------------------------------------------------------- 1 | PROJECTNAME=$(notdir $(shell pwd)) 2 | 3 | 4 | run: 5 | python manage.py runserver_plus 6 | 7 | test: 8 | pytest -s -l --verbose --strict --pylava ${PROJECTNAME} 9 | 10 | migrate: 11 | python manage.py migrate 12 | 13 | req-compile: 14 | @for fl in $(shell ls requirements/); do pip-compile requirements/$${fl}; done 15 | -------------------------------------------------------------------------------- /api/django_blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/account/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from . import models 6 | from . import forms 7 | 8 | 9 | @admin.register(models.User) 10 | class UserAdmin(DjangoUserAdmin): 11 | fieldsets = ( 12 | (None, {"fields": ("password",)}), 13 | (_("Personal info"), {"fields": ("first_name", "last_name", "email")}), 14 | (_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}), 15 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 16 | ) 17 | add_fieldsets = ((None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}),) 18 | list_display = ("email", "first_name", "last_name", "is_staff") 19 | search_fields = ("first_name", "last_name", "email") 20 | ordering = ("email",) 21 | 22 | form = forms.AdminUserChangeForm 23 | -------------------------------------------------------------------------------- /api/django_blog/apps/account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | name = "django_blog.apps.account" 6 | -------------------------------------------------------------------------------- /api/django_blog/apps/account/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import forms 2 | 3 | from . import models 4 | 5 | 6 | class AdminUserChangeForm(forms.UserChangeForm): 7 | class Meta: 8 | model = models.User 9 | fields = "__all__" 10 | -------------------------------------------------------------------------------- /api/django_blog/apps/account/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import BaseUserManager 2 | 3 | from django_blog.apps.common import models as core_models 4 | 5 | 6 | class UserManager(core_models.CoreManager, BaseUserManager): 7 | def get_queryset(self): 8 | return core_models.CoreQuerySet(self.model, using=self._db) 9 | 10 | def create_user(self, email, password=None): 11 | if not email: 12 | raise ValueError("Users must gave an email address") 13 | 14 | user = self.model(email=email) 15 | user.set_password(password) 16 | user.save(using=self._db) 17 | 18 | return user 19 | 20 | def create_superuser(self, email, password): 21 | user = self.create_user(email, password) 22 | user.is_staff = True 23 | user.is_superuser = True 24 | user.save(using=self._db) 25 | 26 | return user 27 | -------------------------------------------------------------------------------- /api/django_blog/apps/account/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-07 09:13 2 | import uuid 3 | 4 | from django.db import migrations, models 5 | from django.utils import timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0009_alter_user_last_name_max_length'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 21 | ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created')), 22 | ('updated', models.DateTimeField(auto_now=True, verbose_name='updated')), 23 | ('password', models.CharField(max_length=128, verbose_name='password')), 24 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 25 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 26 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), 27 | ('first_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='first name')), 28 | ('last_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='last name')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=timezone.now, verbose_name='date joined')), 32 | ('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')), 33 | ('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')), 34 | ], 35 | options={ 36 | 'ordering': ('first_name', 'last_name'), 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /api/django_blog/apps/account/migrations/0002_auto_20190902_1644.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-09-02 16:44 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('account', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='user', 16 | name='date_joined', 17 | field=models.DateTimeField(default=django.utils.timezone.now), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /api/django_blog/apps/account/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/account/migrations/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/account/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin 3 | from django.utils import timezone 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from django_blog.apps.common.models import CoreModel 7 | from . import managers 8 | 9 | 10 | class User(PermissionsMixin, CoreModel, AbstractBaseUser): 11 | email = models.EmailField(verbose_name=_("Email"), unique=True) 12 | first_name = models.CharField(verbose_name=_("first name"), max_length=30, blank=True, null=True) 13 | last_name = models.CharField(verbose_name=_("last name"), max_length=30, blank=True, null=True) 14 | 15 | is_staff = models.BooleanField( 16 | _("staff status"), default=False, help_text=_("Designates whether the user can log into this admin site.") 17 | ) 18 | is_active = models.BooleanField( 19 | _("active"), 20 | default=True, 21 | help_text=_( 22 | "Designates whether this user should be treated as active. " "Unselect this instead of deleting accounts." 23 | ), 24 | ) 25 | 26 | date_joined = models.DateTimeField(default=timezone.now) 27 | 28 | objects = managers.UserManager() 29 | 30 | USERNAME_FIELD = "email" 31 | REQUIRED_FIELDS = [] 32 | 33 | class Meta: 34 | ordering = ("first_name", "last_name") 35 | 36 | def __str__(self): 37 | return self.get_full_name() or self.email 38 | 39 | def get_short_name(self): 40 | if self.first_name: 41 | return self.first_name 42 | return self.email.split("@")[0] 43 | 44 | def get_full_name(self): 45 | return " ".join(filter(None, [self.first_name, self.last_name])) 46 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim,osx,emacs,linux,macos,django,python,windows,pycharm,virtualenv,sublimetext,visualstudiocode 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | media 12 | 13 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 14 | # in your Git repository. Update and uncomment the following line accordingly. 15 | # /staticfiles/ 16 | 17 | ### Emacs ### 18 | # -*- mode: gitignore; -*- 19 | *~ 20 | \#*\# 21 | /.emacs.desktop 22 | /.emacs.desktop.lock 23 | *.elc 24 | auto-save-list 25 | tramp 26 | .\#* 27 | 28 | # Org-mode 29 | .org-id-locations 30 | *_archive 31 | 32 | # flymake-mode 33 | *_flymake.* 34 | 35 | # eshell files 36 | /eshell/history 37 | /eshell/lastdir 38 | 39 | # elpa packages 40 | /elpa/ 41 | 42 | # reftex files 43 | *.rel 44 | 45 | # AUCTeX auto folder 46 | /auto/ 47 | 48 | # cask packages 49 | .cask/ 50 | dist/ 51 | 52 | # Flycheck 53 | flycheck_*.el 54 | 55 | # server auth directory 56 | /server/ 57 | 58 | # projectiles files 59 | .projectile 60 | 61 | # directory configuration 62 | .dir-locals.el 63 | 64 | ### Linux ### 65 | 66 | # temporary files which can be created if a process still has a handle open of a deleted file 67 | .fuse_hidden* 68 | 69 | # KDE directory preferences 70 | .directory 71 | 72 | # Linux trash folder which might appear on any partition or disk 73 | .Trash-* 74 | 75 | # .nfs files are created when an open file is removed but is still being accessed 76 | .nfs* 77 | 78 | ### macOS ### 79 | # General 80 | .DS_Store 81 | .AppleDouble 82 | .LSOverride 83 | 84 | # Icon must end with two \r 85 | Icon 86 | 87 | # Thumbnails 88 | ._* 89 | 90 | # Files that might appear in the root of a volume 91 | .DocumentRevisions-V100 92 | .fseventsd 93 | .Spotlight-V100 94 | .TemporaryItems 95 | .Trashes 96 | .VolumeIcon.icns 97 | .com.apple.timemachine.donotpresent 98 | 99 | # Directories potentially created on remote AFP share 100 | .AppleDB 101 | .AppleDesktop 102 | Network Trash Folder 103 | Temporary Items 104 | .apdisk 105 | 106 | ### OSX ### 107 | # General 108 | 109 | # Icon must end with two \r 110 | 111 | # Thumbnails 112 | 113 | # Files that might appear in the root of a volume 114 | 115 | # Directories potentially created on remote AFP share 116 | 117 | ### PyCharm ### 118 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 119 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 120 | 121 | # User-specific stuff 122 | .idea/**/workspace.xml 123 | .idea/**/tasks.xml 124 | .idea/**/usage.statistics.xml 125 | .idea/**/dictionaries 126 | .idea/**/shelf 127 | 128 | # Generated files 129 | .idea/**/contentModel.xml 130 | 131 | # Sensitive or high-churn files 132 | .idea/**/dataSources/ 133 | .idea/**/dataSources.ids 134 | .idea/**/dataSources.local.xml 135 | .idea/**/sqlDataSources.xml 136 | .idea/**/dynamic.xml 137 | .idea/**/uiDesigner.xml 138 | .idea/**/dbnavigator.xml 139 | 140 | # Gradle 141 | .idea/**/gradle.xml 142 | .idea/**/libraries 143 | 144 | # Gradle and Maven with auto-import 145 | # When using Gradle or Maven with auto-import, you should exclude module files, 146 | # since they will be recreated, and may cause churn. Uncomment if using 147 | # auto-import. 148 | # .idea/modules.xml 149 | # .idea/*.iml 150 | # .idea/modules 151 | 152 | # CMake 153 | cmake-build-*/ 154 | 155 | # Mongo Explorer plugin 156 | .idea/**/mongoSettings.xml 157 | 158 | # File-based project format 159 | *.iws 160 | 161 | # IntelliJ 162 | out/ 163 | 164 | # mpeltonen/sbt-idea plugin 165 | .idea_modules/ 166 | 167 | # JIRA plugin 168 | atlassian-ide-plugin.xml 169 | 170 | # Cursive Clojure plugin 171 | .idea/replstate.xml 172 | 173 | # Crashlytics plugin (for Android Studio and IntelliJ) 174 | com_crashlytics_export_strings.xml 175 | crashlytics.properties 176 | crashlytics-build.properties 177 | fabric.properties 178 | 179 | # Editor-based Rest Client 180 | .idea/httpRequests 181 | 182 | ### PyCharm Patch ### 183 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 184 | 185 | # *.iml 186 | # modules.xml 187 | # .idea/misc.xml 188 | # *.ipr 189 | 190 | # Sonarlint plugin 191 | .idea/sonarlint 192 | 193 | ### Python ### 194 | # Byte-compiled / optimized / DLL files 195 | *.py[cod] 196 | *$py.class 197 | 198 | # C extensions 199 | *.so 200 | 201 | # Distribution / packaging 202 | .Python 203 | develop-eggs/ 204 | downloads/ 205 | eggs/ 206 | .eggs/ 207 | lib/ 208 | lib64/ 209 | parts/ 210 | sdist/ 211 | var/ 212 | wheels/ 213 | *.egg-info/ 214 | .installed.cfg 215 | *.egg 216 | MANIFEST 217 | 218 | # PyInstaller 219 | # Usually these files are written by a python script from a template 220 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 221 | *.manifest 222 | *.spec 223 | 224 | # Installer logs 225 | pip-log.txt 226 | pip-delete-this-directory.txt 227 | 228 | # Unit test / coverage reports 229 | htmlcov/ 230 | .tox/ 231 | .coverage 232 | .coverage.* 233 | .cache 234 | nosetests.xml 235 | coverage.xml 236 | *.cover 237 | .hypothesis/ 238 | .pytest_cache/ 239 | 240 | # Translations 241 | *.mo 242 | 243 | # Django stuff: 244 | 245 | # Flask stuff: 246 | instance/ 247 | .webassets-cache 248 | 249 | # Scrapy stuff: 250 | .scrapy 251 | 252 | # Sphinx documentation 253 | docs/_build/ 254 | 255 | # PyBuilder 256 | target/ 257 | 258 | # Jupyter Notebook 259 | .ipynb_checkpoints 260 | 261 | # IPython 262 | profile_default/ 263 | ipython_config.py 264 | 265 | # pyenv 266 | .python-version 267 | 268 | # celery beat schedule file 269 | celerybeat-schedule 270 | 271 | # SageMath parsed files 272 | *.sage.py 273 | 274 | # Environments 275 | .env 276 | .venv 277 | env/ 278 | venv/ 279 | ENV/ 280 | env.bak/ 281 | venv.bak/ 282 | 283 | # Spyder project settings 284 | .spyderproject 285 | .spyproject 286 | 287 | # Rope project settings 288 | .ropeproject 289 | 290 | # mkdocs documentation 291 | /site 292 | 293 | # mypy 294 | .mypy_cache/ 295 | .dmypy.json 296 | dmypy.json 297 | 298 | ### Python Patch ### 299 | .venv/ 300 | 301 | ### Python.VirtualEnv Stack ### 302 | # Virtualenv 303 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 304 | [Bb]in 305 | [Ii]nclude 306 | [Ll]ib 307 | [Ll]ib64 308 | [Ll]ocal 309 | [Ss]cripts 310 | pyvenv.cfg 311 | pip-selfcheck.json 312 | 313 | ### SublimeText ### 314 | # Cache files for Sublime Text 315 | *.tmlanguage.cache 316 | *.tmPreferences.cache 317 | *.stTheme.cache 318 | 319 | # Workspace files are user-specific 320 | *.sublime-workspace 321 | 322 | # Project files should be checked into the repository, unless a significant 323 | # proportion of contributors will probably not be using Sublime Text 324 | # *.sublime-project 325 | 326 | # SFTP configuration file 327 | sftp-config.json 328 | 329 | # Package control specific files 330 | Package Control.last-run 331 | Package Control.ca-list 332 | Package Control.ca-bundle 333 | Package Control.system-ca-bundle 334 | Package Control.cache/ 335 | Package Control.ca-certs/ 336 | Package Control.merged-ca-bundle 337 | Package Control.user-ca-bundle 338 | oscrypto-ca-bundle.crt 339 | bh_unicode_properties.cache 340 | 341 | # Sublime-github package stores a github token in this file 342 | # https://packagecontrol.io/packages/sublime-github 343 | GitHub.sublime-settings 344 | 345 | ### Vim ### 346 | # Swap 347 | [._]*.s[a-v][a-z] 348 | [._]*.sw[a-p] 349 | [._]s[a-rt-v][a-z] 350 | [._]ss[a-gi-z] 351 | [._]sw[a-p] 352 | 353 | # Session 354 | Session.vim 355 | 356 | # Temporary 357 | .netrwhist 358 | # Auto-generated tag files 359 | tags 360 | # Persistent undo 361 | [._]*.un~ 362 | 363 | ### VirtualEnv ### 364 | # Virtualenv 365 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 366 | 367 | ### VisualStudioCode ### 368 | .vscode/* 369 | !.vscode/settings.json 370 | !.vscode/tasks.json 371 | !.vscode/launch.json 372 | !.vscode/extensions.json 373 | 374 | ### Windows ### 375 | # Windows thumbnail cache files 376 | Thumbs.db 377 | ehthumbs.db 378 | ehthumbs_vista.db 379 | 380 | # Dump file 381 | *.stackdump 382 | 383 | # Folder config file 384 | [Dd]esktop.ini 385 | 386 | # Recycle Bin used on file shares 387 | $RECYCLE.BIN/ 388 | 389 | # Windows Installer files 390 | *.cab 391 | *.msi 392 | *.msix 393 | *.msm 394 | *.msp 395 | 396 | # Windows shortcuts 397 | *.lnk 398 | 399 | 400 | # End of https://www.gitignore.io/api/vim,osx,emacs,linux,macos,django,python,windows,pycharm,virtualenv,sublimetext,visualstudiocode 401 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/blog/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | 4 | # Register your models here. 5 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = "django_blog.apps.blog" 6 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-09-08 15:34 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Tag', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('name', models.CharField(max_length=100, unique=True)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Post', 27 | fields=[ 28 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 29 | ('created', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created')), 30 | ('updated', models.DateTimeField(auto_now=True, verbose_name='updated')), 31 | ('is_active', models.BooleanField(db_index=True, default=True)), 32 | ('title', models.CharField(max_length=100)), 33 | ('text', models.TextField()), 34 | ('image', models.ImageField(blank=True, null=True, upload_to='')), 35 | ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)), 36 | ('tags', models.ManyToManyField(blank=True, related_name='posts', to='blog.Tag')), 37 | ], 38 | options={ 39 | 'abstract': False, 40 | }, 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/blog/migrations/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/blog/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .post import Post, Tag 2 | 3 | 4 | __all__ = ['Post', 'Tag'] 5 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/models/post.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_blog.apps.account.models import User 4 | from django_blog.apps.common.models import CoreModel 5 | 6 | 7 | class Tag(models.Model): 8 | name = models.CharField(max_length=100, unique=True) 9 | 10 | 11 | class Post(CoreModel): 12 | title = models.CharField(max_length=100) 13 | text = models.TextField() 14 | tags = models.ManyToManyField(Tag, related_name='posts', blank=True) 15 | author = models.ForeignKey(User, related_name='posts', on_delete=models.CASCADE, blank=True, null=True) 16 | image = models.ImageField(null=True, blank=True) 17 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/rest_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/blog/rest_api/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/blog/rest_api/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/blog/rest_api/serializers/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/blog/rest_api/serializers/post.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from django_blog.apps.account.models import User 4 | from django_blog.apps.blog.models import Tag, Post 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = User 10 | fields = ('pk', 'email', 'first_name', 'last_name',) 11 | 12 | 13 | class TagSerializer(serializers.ModelSerializer): 14 | class Meta: 15 | model = Tag 16 | fields = ('pk', 'name',) 17 | 18 | 19 | class PostSerializer(serializers.ModelSerializer): 20 | tags = TagSerializer(many=True, required=False, read_only=True) 21 | author = UserSerializer(required=False, read_only=True) 22 | serializers.ImageField(use_url=True, required=False, allow_null=True) 23 | 24 | class Meta: 25 | model = Post 26 | fields = ('pk', 'title', 'text', 'tags', 'author', 'image',) 27 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/rest_api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import blog_views 4 | 5 | urlpatterns = [ 6 | path('posts/', blog_views.PostListCreateAPIView.as_view(), name='api-post-list'), 7 | path('posts//', blog_views.PostDetailsAPIView.as_view(), name='api-post-details'), 8 | ] 9 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/rest_api/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/blog/rest_api/views/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/blog/rest_api/views/blog_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView 2 | 3 | from django_blog.apps.blog.models import Post 4 | from django_blog.apps.blog.rest_api.serializers.post import PostSerializer 5 | 6 | 7 | class PostListCreateAPIView(ListCreateAPIView): 8 | """ 9 | API view to retrieve list of posts or create new 10 | """ 11 | serializer_class = PostSerializer 12 | queryset = Post.objects.active() 13 | 14 | 15 | class PostDetailsAPIView(RetrieveUpdateDestroyAPIView): 16 | """ 17 | API view to retrieve, update or delete post 18 | """ 19 | serializer_class = PostSerializer 20 | queryset = Post.objects.active() 21 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/blog/tests/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/blog/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django_blog.apps.blog.models import Post, Tag 4 | 5 | 6 | class PostTestCase(TestCase): 7 | def test_post(self): 8 | self.assertEquals( 9 | Post.objects.count(), 10 | 0 11 | ) 12 | Post.objects.create( 13 | title='active', text='text', is_active=True 14 | ) 15 | Post.objects.create( 16 | title='inactive', text='text', is_active=False 17 | ) 18 | self.assertEquals( 19 | Post.objects.count(), 20 | 2 21 | ) 22 | active_posts = Post.objects.active() 23 | self.assertEquals( 24 | active_posts.count(), 25 | 1 26 | ) 27 | inactive_posts = Post.objects.inactive() 28 | self.assertEquals( 29 | inactive_posts.count(), 30 | 1 31 | ) 32 | 33 | 34 | class TagTestCase(TestCase): 35 | def test_tag(self): 36 | self.assertEquals( 37 | Tag.objects.count(), 38 | 0 39 | ) 40 | Tag.objects.create(name='name') 41 | self.assertEquals( 42 | Tag.objects.count(), 43 | 1 44 | ) 45 | -------------------------------------------------------------------------------- /api/django_blog/apps/blog/tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from rest_framework.test import APITestCase 4 | from rest_framework.views import status 5 | 6 | from django_blog.apps.blog.models import Post, Tag 7 | 8 | 9 | class PostListCreateAPIView(APITestCase): 10 | def setUp(self) -> None: 11 | self.url = reverse('api-post-list', kwargs={'version': 'v1'}) 12 | 13 | def test_create_post(self): 14 | self.assertEquals( 15 | Post.objects.count(), 16 | 0 17 | ) 18 | data = { 19 | 'title': 'title', 20 | 'text': 'text' 21 | } 22 | response = self.client.post(self.url, data=data, format='json') 23 | self.assertEquals(response.status_code, status.HTTP_201_CREATED) 24 | self.assertEquals( 25 | Post.objects.count(), 26 | 1 27 | ) 28 | post = Post.objects.first() 29 | self.assertEquals( 30 | post.title, 31 | data['title'] 32 | ) 33 | self.assertEquals( 34 | post.text, 35 | data['text'] 36 | ) 37 | 38 | def test_get_post_list(self): 39 | tag = Tag(name='tag_name') 40 | tag.save() 41 | post = Post(title='title1', text='text1') 42 | post.save() 43 | post.tags.add(tag) 44 | 45 | response = self.client.get(self.url) 46 | response_json = response.json() 47 | self.assertEquals( 48 | response.status_code, 49 | status.HTTP_200_OK 50 | ) 51 | self.assertEquals( 52 | len(response_json), 53 | 1 54 | ) 55 | data = response_json[0] 56 | self.assertEquals( 57 | data['title'], 58 | post.title 59 | ) 60 | self.assertEquals( 61 | data['text'], 62 | post.text 63 | ) 64 | self.assertEquals( 65 | data['tags'][0]['name'], 66 | tag.name 67 | ) 68 | 69 | 70 | class PostDetailsAPIViewTest(APITestCase): 71 | def setUp(self) -> None: 72 | self.post = Post(title='title2', text='text2') 73 | self.post.save() 74 | self.url = reverse('api-post-details', kwargs={'version': 'v1', 'pk': self.post.pk}) 75 | 76 | def test_get_post_details(self): 77 | response = self.client.get(self.url) 78 | self.assertEquals( 79 | response.status_code, 80 | status.HTTP_200_OK 81 | ) 82 | data = response.json() 83 | self.assertEquals( 84 | data['pk'], 85 | str(self.post.pk) 86 | ) 87 | self.assertEquals( 88 | data['title'], 89 | self.post.title 90 | ) 91 | self.assertEquals( 92 | data['text'], 93 | self.post.text 94 | ) 95 | 96 | def test_update_post(self): 97 | response = self.client.get(self.url) 98 | self.assertEquals( 99 | response.status_code, 100 | status.HTTP_200_OK 101 | ) 102 | data = response.json() 103 | data['title'] = 'new_title' 104 | data['text'] = 'new_text' 105 | response = self.client.put(self.url, data=data, format='json') 106 | self.assertEquals( 107 | response.status_code, 108 | status.HTTP_200_OK 109 | ) 110 | self.post.refresh_from_db() 111 | self.assertEquals( 112 | self.post.title, 113 | data['title'] 114 | ) 115 | self.assertEquals( 116 | self.post.text, 117 | data['text'] 118 | ) 119 | 120 | def test_delete_post(self): 121 | self.assertEquals( 122 | Post.objects.count(), 123 | 1 124 | ) 125 | response = self.client.delete(self.url) 126 | self.assertEquals( 127 | response.status_code, 128 | status.HTTP_204_NO_CONTENT 129 | ) 130 | self.assertEquals( 131 | Post.objects.count(), 132 | 0 133 | ) 134 | -------------------------------------------------------------------------------- /api/django_blog/apps/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/common/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/common/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommonConfig(AppConfig): 5 | name = "django_blog.apps.common" 6 | -------------------------------------------------------------------------------- /api/django_blog/apps/common/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/common/management/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/common/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/apps/common/management/commands/__init__.py -------------------------------------------------------------------------------- /api/django_blog/apps/common/management/commands/generate_secretkey.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from django.core.management.base import BaseCommand 4 | from django.utils.crypto import get_random_string 5 | 6 | 7 | class Command(BaseCommand): 8 | def add_arguments(self, parser): 9 | parser.add_argument("length", type=int) 10 | 11 | def handle(self, *args, **options): 12 | print( 13 | get_random_string( 14 | length=options["length"], allowed_chars=string.ascii_letters + string.digits + string.punctuation 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /api/django_blog/apps/common/management/commands/startapp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.core.management.base import CommandError 6 | from django.core.management.commands import startapp 7 | 8 | 9 | DS_BE_APP_TEMPLATE = "https://be.skeletons.djangostars.com/djangostars_app_template__django{extensions}.tar.gz" 10 | 11 | 12 | class Command(startapp.Command): 13 | def handle(self, **options): 14 | app_name = options.pop("name") 15 | 16 | directory = os.path.join(settings.BASE_DIR, "apps", app_name) 17 | if os.path.exists(directory): 18 | raise CommandError('App with name "{}" already exists'.format(app_name)) 19 | 20 | os.mkdir(directory) 21 | 22 | options["template"] = self.get_template() 23 | options["directory"] = directory 24 | options["project_name"] = settings.ROOT_URLCONF.split(".", 1)[0] 25 | 26 | try: 27 | super(Command, self).handle(name=app_name, **options) 28 | except CommandError as ex: 29 | shutil.rmtree(directory) 30 | raise ex 31 | 32 | @staticmethod 33 | def get_template(): 34 | extensions = [] 35 | if "rest_framework" in settings.INSTALLED_APPS: 36 | extensions.append("drf") 37 | 38 | return DS_BE_APP_TEMPLATE.format(extensions="_{}".format("_".join(extensions)) if len(extensions) > 0 else "") 39 | -------------------------------------------------------------------------------- /api/django_blog/apps/common/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import CoreModel, CoreManager, CoreQuerySet 2 | 3 | 4 | __all__ = ["CoreModel", "CoreManager", "CoreQuerySet"] 5 | -------------------------------------------------------------------------------- /api/django_blog/apps/common/models/core.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class CoreQuerySet(models.QuerySet): 8 | def active(self): 9 | return self.filter(is_active=True) 10 | 11 | def inactive(self): 12 | return self.filter(is_active=False) 13 | 14 | 15 | class CoreManager(models.Manager): 16 | def get_queryset(self): 17 | return CoreQuerySet(self.model, using=self._db) 18 | 19 | def active(self): 20 | return self.get_queryset().active() 21 | 22 | def inactive(self): 23 | return self.get_queryset().inactive() 24 | 25 | 26 | class CoreModel(models.Model): 27 | 28 | uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) 29 | created = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_("created")) 30 | updated = models.DateTimeField(auto_now=True, verbose_name=_("updated")) 31 | is_active = models.BooleanField(default=True, db_index=True) 32 | 33 | objects = CoreManager() 34 | 35 | class Meta: 36 | abstract = True 37 | 38 | def __str__(self): 39 | return str(self.pk) 40 | 41 | def __repr__(self): 42 | return f"<{self.__class__.__name__} {self.pk}>" 43 | 44 | def activate(self): 45 | if not self.is_active: 46 | self.is_active = True 47 | self.save(update_fields=["is_active", "updated"] if self.pk else None) 48 | 49 | def deactivate(self): 50 | if self.is_active: 51 | self.is_active = False 52 | self.save(update_fields=["is_active", "updated"] if self.pk else None) 53 | -------------------------------------------------------------------------------- /api/django_blog/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .django import * # noqa 2 | from .contrib import * # noqa 3 | from .django_blog import * # noqa 4 | -------------------------------------------------------------------------------- /api/django_blog/settings/contrib.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/settings/contrib.py -------------------------------------------------------------------------------- /api/django_blog/settings/django.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_blog project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | from .environment import env 16 | 17 | 18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | 21 | 22 | def rel(*path): 23 | return os.path.join(BASE_DIR, *path) 24 | 25 | 26 | # Quick-start development settings - unsuitable for production 27 | # See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/ 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = env.bool("DJANGO_BLOG_DEBUG") 31 | 32 | ALLOWED_HOSTS = env.list("DJANGO_BLOG_ALLOWED_HOSTS", default=[]) 33 | 34 | SECRET_KEY = env.str("DJANGO_BLOG_SECRET_KEY") 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | # django apps 40 | "django.contrib.admin", 41 | "django.contrib.auth", 42 | "django.contrib.contenttypes", 43 | "django.contrib.sessions", 44 | "django.contrib.messages", 45 | "django.contrib.staticfiles", 46 | # 3rd party apps 47 | "rest_framework", 48 | "drf_yasg", 49 | # our apps 50 | "django_blog.apps.common.apps.CommonConfig", 51 | "django_blog.apps.account.apps.AccountConfig", 52 | "django_blog.apps.blog.apps.BlogConfig", 53 | 54 | ] + env.list("DJANGO_BLOG_DEV_INSTALLED_APPS", default=[]) 55 | 56 | MIDDLEWARE = [ 57 | "django.middleware.security.SecurityMiddleware", 58 | "django.contrib.sessions.middleware.SessionMiddleware", 59 | "django.middleware.common.CommonMiddleware", 60 | "django.middleware.csrf.CsrfViewMiddleware", 61 | "django.contrib.auth.middleware.AuthenticationMiddleware", 62 | "django.contrib.messages.middleware.MessageMiddleware", 63 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 64 | ] + env.list("DJANGO_BLOG_DEV_MIDDLEWARE", default=[]) 65 | 66 | ROOT_URLCONF = "django_blog.urls" 67 | 68 | TEMPLATES = [ 69 | { 70 | "BACKEND": "django.template.backends.django.DjangoTemplates", 71 | "DIRS": [rel("templates/")], 72 | "APP_DIRS": True, 73 | "OPTIONS": { 74 | "context_processors": [ 75 | "django.template.context_processors.debug", 76 | "django.template.context_processors.request", 77 | "django.contrib.auth.context_processors.auth", 78 | "django.contrib.messages.context_processors.messages", 79 | ] 80 | }, 81 | } 82 | ] 83 | 84 | WSGI_APPLICATION = "django_blog.wsgi.application" 85 | 86 | 87 | # Database 88 | # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases 89 | 90 | DATABASES = {"default": env.db("DJANGO_BLOG_DATABASE_URL")} 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators 95 | 96 | AUTH_USER_MODEL = "account.User" 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, 100 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 101 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 102 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 103 | ] 104 | 105 | SESSION_COOKIE_SECURE = env.bool("DJANGO_BLOG_SESSION_COOKIE_SECURE", default=True) 106 | SESSION_COOKIE_NAME = "s" 107 | CSRF_COOKIE_NAME = "c" 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/ 112 | 113 | LANGUAGE_CODE = "en-us" 114 | 115 | TIME_ZONE = "UTC" 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/ 126 | 127 | STATIC_URL = "/static/" 128 | STATIC_ROOT = rel("staticfiles/") 129 | STATICFILES_DIRS = (rel("static/"),) 130 | 131 | MEDIA_URL = "/media/" 132 | MEDIA_ROOT = rel("media/") 133 | 134 | REST_FRAMEWORK = { 135 | 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning' 136 | } 137 | -------------------------------------------------------------------------------- /api/django_blog/settings/django_blog.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olegkovalov/django_blog/adbc53ef6577dfd8c48b639caae915a2772a0aa8/api/django_blog/settings/django_blog.py -------------------------------------------------------------------------------- /api/django_blog/settings/environment.py: -------------------------------------------------------------------------------- 1 | import environ 2 | 3 | env = environ.Env(DEBUG=(bool, False)) 4 | environ.Env.read_env(".env") 5 | -------------------------------------------------------------------------------- /api/django_blog/urls.py: -------------------------------------------------------------------------------- 1 | """django_blog URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/{{ docs_version }}/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.conf import settings 18 | from django.urls import path, include, re_path 19 | 20 | from drf_yasg.views import get_schema_view 21 | from drf_yasg import openapi 22 | from rest_framework import permissions 23 | 24 | 25 | schema_view = get_schema_view( 26 | openapi.Info( 27 | title="Blog API", 28 | default_version='v1', 29 | description="Test description", 30 | ), 31 | public=True, 32 | permission_classes=(permissions.AllowAny,), 33 | ) 34 | 35 | urlpatterns = [ 36 | path('admin/', admin.site.urls), 37 | path('doc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), 38 | re_path(r'api/(?P[v1|v2]+)/', include('django_blog.apps.blog.rest_api.urls')), 39 | ] 40 | 41 | 42 | # enable serve static by django for local development 43 | if settings.DEBUG: # noqa 44 | from django.conf.urls.static import static 45 | 46 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 47 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 48 | 49 | 50 | # enable debug_toolbar for local development (if installed) 51 | if settings.DEBUG and "debug_toolbar" in settings.INSTALLED_APPS: 52 | import debug_toolbar 53 | 54 | urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] 55 | -------------------------------------------------------------------------------- /api/django_blog/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_blog project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_blog.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_blog.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /api/pylama.ini: -------------------------------------------------------------------------------- 1 | [pylama] 2 | format = pycodestyle 3 | skip = */.tox/*,*/.env/*,*/migrations/* 4 | linters = pycodestyle,pyflakes,mccabe 5 | 6 | [pylama:pycodestyle] 7 | max_line_length = 120 8 | 9 | [pylama:pylint] 10 | max_line_length = 120 11 | load-plugins = pylint_django -------------------------------------------------------------------------------- /api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | include = '\.pyi?$' 3 | line-length = 120 4 | exclude = ''' 5 | ( 6 | /( 7 | \.git 8 | |\.tox 9 | |migrations 10 | )/ 11 | ) 12 | ''' 13 | -------------------------------------------------------------------------------- /api/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | bandit 4 | bandit_targets = django_blog 5 | bandit_recurse = true 6 | 7 | DJANGO_SETTINGS_MODULE = django_blog.settings 8 | # -- recommended but optional: 9 | python_files = tests.py test_*.py *_test 10 | -------------------------------------------------------------------------------- /api/requirements/common.in: -------------------------------------------------------------------------------- 1 | django 2 | psycopg2-binary 3 | django-environ 4 | gunicorn 5 | djangorestframework 6 | -------------------------------------------------------------------------------- /api/requirements/common.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile /tmp/tmp.be_skeleton_builder/api/requirements/common.in 6 | # 7 | django-environ==0.4.5 8 | django==2.2.4 9 | djangorestframework==3.10.2 10 | gunicorn==19.9.0 11 | psycopg2-binary==2.8.3 12 | pytz==2019.2 # via django 13 | sqlparse==0.3.0 # via django 14 | Pillow==6.1.0 15 | drf-yasg==1.16.1 16 | -------------------------------------------------------------------------------- /api/requirements/dev.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | 3 | pip-tools 4 | black 5 | bandit 6 | pytest-django 7 | pytest-cov 8 | pytest-bandit 9 | pytest 10 | pylama 11 | django-extensions 12 | Werkzeug 13 | ipython 14 | ipdb 15 | django-debug-toolbar 16 | Pillow 17 | -------------------------------------------------------------------------------- /api/requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile /tmp/tmp.be_skeleton_builder/api/requirements/dev.in 6 | # 7 | appdirs==1.4.3 # via black 8 | atomicwrites==1.3.0 # via pytest 9 | attrs==19.1.0 # via black, packaging, pytest 10 | backcall==0.1.0 # via ipython 11 | bandit==1.6.2 12 | black==19.3b0 13 | click==7.0 # via black, pip-tools 14 | coverage==4.5.4 # via pytest-cov 15 | decorator==4.4.0 # via ipython, traitlets 16 | django-debug-toolbar==2.0 17 | django-environ==0.4.5 18 | django-extensions==2.2.1 19 | django==2.2.4 20 | djangorestframework==3.10.2 21 | gitdb2==2.0.5 # via gitpython 22 | gitpython==3.0.2 # via bandit 23 | gunicorn==19.9.0 24 | importlib-metadata==0.19 # via pluggy, pytest 25 | ipdb==0.12.2 26 | ipython-genutils==0.2.0 # via traitlets 27 | ipython==7.7.0 28 | jedi==0.15.1 # via ipython 29 | mccabe==0.6.1 # via pylama 30 | more-itertools==7.2.0 # via pytest, zipp 31 | packaging==19.1 # via pytest 32 | parso==0.5.1 # via jedi 33 | pbr==5.4.2 # via stevedore 34 | pexpect==4.7.0 # via ipython 35 | pickleshare==0.7.5 # via ipython 36 | pip-tools==4.1.0 37 | pluggy==0.12.0 # via pytest 38 | prompt-toolkit==2.0.9 # via ipython 39 | psycopg2-binary==2.8.3 40 | ptyprocess==0.6.0 # via pexpect 41 | py==1.8.0 # via pytest 42 | pycodestyle==2.5.0 # via pylama 43 | pydocstyle==4.0.1 # via pylama 44 | pyflakes==2.1.1 # via pylama 45 | pygments==2.4.2 # via ipython 46 | pylama==7.7.1 47 | pyparsing==2.4.2 # via packaging 48 | pytest-bandit==0.5.2 49 | pytest-cov==2.7.1 50 | pytest-django==3.5.1 51 | pytest==5.1.1 52 | pytz==2019.2 # via django 53 | pyyaml==5.1.2 # via bandit 54 | six==1.12.0 # via bandit, django-extensions, packaging, pip-tools, prompt-toolkit, stevedore, traitlets 55 | smmap2==2.0.5 # via gitdb2 56 | snowballstemmer==1.9.0 # via pydocstyle 57 | sqlparse==0.3.0 # via django, django-debug-toolbar 58 | stevedore==1.30.1 # via bandit 59 | toml==0.10.0 # via black 60 | traitlets==4.3.2 # via ipython 61 | wcwidth==0.1.7 # via prompt-toolkit, pytest 62 | werkzeug==0.15.5 63 | zipp==0.6.0 # via importlib-metadata 64 | 65 | # The following packages are considered to be unsafe in a requirements file: 66 | # setuptools==41.2.0 # via ipdb, ipython 67 | -------------------------------------------------------------------------------- /build/Dockerfile.api: -------------------------------------------------------------------------------- 1 | FROM python:latest 2 | 3 | WORKDIR /api 4 | 5 | COPY api/ /api/ 6 | COPY build/docker-entrypoint-api.sh /api/ 7 | 8 | RUN ["chmod", "+x", "/api/docker-entrypoint-api.sh"] 9 | RUN ["pip", "install", "-r", "requirements/dev.txt"] 10 | 11 | ENTRYPOINT ["/api/docker-entrypoint-api.sh"] 12 | 13 | EXPOSE 8000 14 | 15 | RUN ["pwd"] 16 | RUN ["ls", "-l"] 17 | 18 | CMD ["gunicorn", "django_blog.wsgi", "-b", "0.0.0.0:8000"] -------------------------------------------------------------------------------- /build/ci/circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | api: 5 | docker: 6 | - image: circleci/python:stretch-browsers-legacy 7 | - image: circleci/postgres:alpine-postgis-ram 8 | working_directory: ~/app 9 | environment: 10 | - DJANGO_BLOG_DEBUG=off 11 | - DJANGO_BLOG_DATABASE_URL=psql://root@localhost:5432/circle_test 12 | - DJANGO_BLOG_SECRET_KEY=1234567890 13 | steps: 14 | - checkout 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "api/requirements/dev.txt" }} 18 | # fallback to using the latest cache if no exact match is found 19 | - v1-dependencies- 20 | - run: 21 | name: Install python packages 22 | command: | 23 | python3 -m venv venv 24 | . venv/bin/activate 25 | pip3 install -r api/requirements/dev.txt 26 | - save_cache: 27 | paths: 28 | - ./venv 29 | key: v1-dependencies-{{ checksum "api/requirements/dev.txt" }} 30 | - run: 31 | name: Python version 32 | command: python --version 33 | - run: 34 | name: Codestyle checking by Black 35 | working_directory: api 36 | when: always 37 | command: | 38 | . ../venv/bin/activate 39 | black --check django_blog 40 | - run: 41 | name: Run tests 42 | working_directory: api 43 | when: always 44 | command: | 45 | . ../venv/bin/activate 46 | pytest -s -l --verbose --strict \ 47 | --pylama \ 48 | --bandit \ 49 | --junitxml=../tests_artifacts_be/junit/results.xml \ 50 | --cov=django_blog --cov-report=term --cov-report=html:../tests_artifacts_be/cov_html --cov-report=xml:../tests_artifacts_be/coverage/results.xml \ 51 | django_blog 52 | - store_test_results: 53 | path: ../tests_artifacts_be 54 | destination: tests_artifacts 55 | - store_artifacts: 56 | path: ../tests_artifacts_be 57 | destination: tests_artifacts 58 | 59 | workflows: 60 | version: 2 61 | 62 | django_blog: 63 | jobs: 64 | - api 65 | -------------------------------------------------------------------------------- /build/docker-compose-api.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | api_django_blog: 5 | build: 6 | context: .. 7 | dockerfile: build/Dockerfile.api 8 | depends_on: 9 | - postgres 10 | environment: 11 | - DJANGO_BLOG_DATABASE_URL=psql://django_blog:django_blog@postgres:5432/django_blog 12 | ports: 13 | - "8000:8000" 14 | postgres: 15 | image: postgres:latest 16 | volumes: 17 | - "django_blog-pgdata:/var/lib/postgresql/data" 18 | environment: 19 | - POSTGRES_DB=django_blog 20 | - POSTGRES_USER=django_blog 21 | - POSTGRES_PASSWORD=django_blog 22 | ports: 23 | - "5432:5432" 24 | # redis: 25 | # image: redis:latest 26 | # elastic: 27 | # image: elasticsearch:7.2.0 28 | # volumes: 29 | # - "django_blog-elasticdata:/usr/share/elasticsearch/data" 30 | # environment: 31 | # - discovery.type=single-node 32 | # ports: 33 | # - "9200:9200" 34 | # - "9300:9200" 35 | 36 | volumes: 37 | django_blog-pgdata: 38 | django_blog-elasticdata: 39 | -------------------------------------------------------------------------------- /build/docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres:latest 6 | volumes: 7 | - "django_blog-pgdata:/var/lib/postgresql/data" 8 | environment: 9 | - POSTGRES_DB=django_blog 10 | - POSTGRES_USER=django_blog 11 | - POSTGRES_PASSWORD=django_blog 12 | ports: 13 | - "5432:5432" 14 | # redis: 15 | # image: redis:latest 16 | # elastic: 17 | # image: elasticsearch:7.2.0 18 | # volumes: 19 | # - "django_blog-elasticdata:/usr/share/elasticsearch/data" 20 | # environment: 21 | # - discovery.type=single-node 22 | # ports: 23 | # - "9200:9200" 24 | # - "9300:9200" 25 | 26 | volumes: 27 | django_blog-pgdata: 28 | django_blog-elasticdata: 29 | -------------------------------------------------------------------------------- /build/docker-entrypoint-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /api/ 4 | 5 | python manage.py migrate 6 | 7 | exec "$@" -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | build/ci/circle.yml --------------------------------------------------------------------------------