├── .gitignore ├── LICENSE ├── README.md ├── cbv_tutorial ├── __init__.py ├── fixtures │ └── users.json ├── settings.py ├── urls.py └── wsgi.py ├── core ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── mixins.py ├── models.py ├── templatetags │ ├── __init__.py │ └── custom_tags.py ├── tests.py ├── urls.py └── views.py ├── djangocbv ├── __init__.py ├── admin.py ├── apps.py ├── filters.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── mixins.py ├── models.py ├── templates │ ├── _categories_partial.html │ ├── django_cbv_home.html │ └── djangocbv │ │ ├── _article_content_partial.html │ │ ├── article_detail.html │ │ ├── article_form.html │ │ ├── article_list.html │ │ ├── category_detail.html │ │ ├── category_form.html │ │ ├── category_list.html │ │ ├── document_detail.html │ │ ├── document_form.html │ │ └── document_list.html ├── tests.py ├── urls.py └── views.py ├── manage.py ├── requirements.txt ├── rsp.bat ├── run.bat └── templates ├── _partial_account.html ├── _partial_messages.html ├── about.html ├── help.html ├── home.html ├── registration ├── logged_out.html └── login.html └── site_base.html /.gitignore: -------------------------------------------------------------------------------- 1 | ### Django ### 2 | *.log 3 | *.pot 4 | *.pyc 5 | __pycache__/ 6 | local_settings.py 7 | *.pyc 8 | *.swp 9 | *.swo 10 | *.bz 11 | *.b 12 | .hg 13 | .ropeproject 14 | coverage_html_report 15 | *.db 16 | .idea 17 | .vagrant 18 | local.py 19 | db.sqlite3 20 | media -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cbv-tutorial 2 | A tutorial project for django class based views 3 | 4 | This is an accompanying project to my article about Django CBVs. You can find it @ https://spapas.github.io/2018/03/19/comprehensive-django-cbv-guide/ 5 | 6 | -------------------------------------------------------------------------------- /cbv_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/cbv-tutorial/ae6f8a1011617bb4db0fdb648a6d0ec64f73f950/cbv_tutorial/__init__.py -------------------------------------------------------------------------------- /cbv_tutorial/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [{"model": "auth.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$100000$7288zvUtv042$ROHKffXGxogMoVkRTaK4/fe/UtxZXIj3h52UAksH1hg=", "last_login": "2018-03-14T08:42:00.870Z", "is_superuser": true, "username": "root", "first_name": "", "last_name": "", "email": "r@r.gr", "is_staff": true, "is_active": true, "date_joined": "2018-03-13T09:09:02.233Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 2, "fields": {"password": "pbkdf2_sha256$100000$vzIPSMWe1E8D$Vz3nFl6txnzUAkcp8k2eXREaGMzafD27Y+PeOHifx3k=", "last_login": "2018-03-13T11:06:24Z", "is_superuser": false, "username": "user1", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2018-03-13T09:30:09Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 3, "fields": {"password": "pbkdf2_sha256$100000$QffXp41s9E0V$VxL8ZtT1Bbu499iy5fHqMdGwxz+hMIo4OLuimWNKxsI=", "last_login": "2018-03-13T09:40:05Z", "is_superuser": false, "username": "publisher1", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2018-03-13T09:30:18Z", "groups": [], "user_permissions": [32]}}, {"model": "auth.user", "pk": 4, "fields": {"password": "pbkdf2_sha256$100000$29QvCb20RC22$57eCRDAOy0Jyt8ZzqMjh7Lkjn+F22REhN4SmdnlpWio=", "last_login": "2018-03-13T09:44:42Z", "is_superuser": false, "username": "admin1", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2018-03-13T09:30:23Z", "groups": [], "user_permissions": [33]}}, {"model": "auth.user", "pk": 5, "fields": {"password": "pbkdf2_sha256$100000$yr7Cs155wyAK$GEpQax/nQD66W+6fNe4T0TdYbYHTs+lZw1wb7P4GFJY=", "last_login": null, "is_superuser": false, "username": "user2", "first_name": "", "last_name": "", "email": "", "is_staff": false, "is_active": true, "date_joined": "2018-03-14T08:42:22.202Z", "groups": [], "user_permissions": []}}] -------------------------------------------------------------------------------- /cbv_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for cbv_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'lq374_-(&rz=d&kn^+y*hf@e17o^(9i%qq(os_59ka(o2q#53m' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'django_extensions', 42 | 'django_filters', 43 | 44 | 'core', 45 | 'djangocbv', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'cbv_tutorial.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'cbv_tutorial.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 108 | LANGUAGE_CODE = 'en-us' 109 | TIME_ZONE = 'UTC' 110 | USE_I18N = True 111 | USE_L10N = True 112 | USE_TZ = True 113 | LOGIN_REDIRECT_URL = '/' 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 117 | STATIC_URL = '/static/' 118 | MEDIA_URL = '/media/' 119 | 120 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 121 | -------------------------------------------------------------------------------- /cbv_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | from django.contrib.auth.views import LoginView, LogoutView 4 | from django.views.generic import TemplateView 5 | from django.conf import settings 6 | from django.conf.urls.static import static 7 | 8 | 9 | class HomeTemplateView(TemplateView): 10 | template_name ='home.html' 11 | 12 | 13 | urlpatterns = [ 14 | url(r'^admin/', admin.site.urls), 15 | url(r'^accounts/login/', LoginView.as_view(), name='login', ), 16 | url(r'^accounts/logout/', LogoutView.as_view(), name='logout', ), 17 | 18 | url(r'^$', HomeTemplateView.as_view(), name='home', ), 19 | url(r'^non-django-cbv/', include('core.urls')), 20 | url(r'^djangocbv/', include('djangocbv.urls')), 21 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 22 | -------------------------------------------------------------------------------- /cbv_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cbv_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/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", "cbv_tutorial.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/cbv-tutorial/ae6f8a1011617bb4db0fdb648a6d0ec64f73f950/core/__init__.py -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | # Register your models here. 7 | -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class CoreConfig(AppConfig): 8 | name = 'core' 9 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/cbv-tutorial/ae6f8a1011617bb4db0fdb648a6d0ec64f73f950/core/migrations/__init__.py -------------------------------------------------------------------------------- /core/mixins.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | class DefaultHeaderMixin: 4 | def get_header(self, ): 5 | return self.header if self.header else "DEFAULT HEADER" 6 | 7 | 8 | class DefaultContextMixin: 9 | def get_context(self, ): 10 | return self.context if self.context else ["DEFAULT CONTEXT"] 11 | 12 | 13 | class HeaderPrefixMixin: 14 | def get_header(self, ): 15 | return "PREFIX: " + super().get_header() 16 | 17 | 18 | class DefaultHeaderSuperMixin: 19 | def get_header(self, ): 20 | return super().get_header() if super().get_header() else "DEFAULT HEADER" 21 | 22 | 23 | class ExtraContext1Mixin: 24 | def get_context(self, ): 25 | ctx = super().get_context() 26 | ctx.append('data1') 27 | return ctx 28 | 29 | 30 | class ExtraContext2Mixin: 31 | def get_context(self, ): 32 | ctx = super().get_context() 33 | ctx.insert(0, 'data2') 34 | return ctx 35 | 36 | 37 | class UrlPatternsMixin: 38 | def render_patterns(self): 39 | ctx = "
".join( 40 | ['{1}'.format(reverse(p.name), p.name) for p in self.get_urlpatterns()] 41 | ) 42 | return ctx -------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/cbv-tutorial/ae6f8a1011617bb4db0fdb648a6d0ec64f73f950/core/templatetags/__init__.py -------------------------------------------------------------------------------- /core/templatetags/custom_tags.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django import template 3 | 4 | register = template.Library() 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | from . import views 5 | 6 | urlpatterns = [ 7 | url(r'^$', views.HomeCustomClassView.as_view(), name='home-ccv'), 8 | url(r'^ccv-empty/$', views.CustomClassView.as_view(), name='ccv-empty'), 9 | url(r'^ccv-with-values/$', views.CustomClassView.as_view(header='Hello', context=['hello', 'world', ], footer='Bye', ), name='ccv-with-values'), 10 | url(r'^ccv-inherits/$', views.InheritsCustomClassView.as_view(), name='ccv-inherits'), 11 | url(r'^default-header-bccv/$', views.DefaultHeaderBetterCustomClassView.as_view(), name='default-header-bccv'), 12 | url(r'^json-ccv/$', views.JsonCustomClassView.as_view(), name='json-ccv'), 13 | url(r'^default-header-json-ccv/$', views.DefaultHeaderJsonCustomClassView.as_view(), name='default-header-json-ccv'), 14 | url(r'^json-default-header-ccv/$', views.JsonDefaultHeaderCustomClassView.as_view(), name='json-default-header-ccv'), 15 | url(r'^default-header-context-ccv/$', views.DefaultHeaderContextCustomClassView.as_view(), name='default-header-context-ccv'), 16 | url(r'^default-header-mixin-bccv/$', views.DefaultHeaderMixinBetterCustomClassView.as_view(), name='default-header-mixin-bccv'), 17 | url(r'^default-context-mixin-bccv/$', views.DefaultContextMixinBetterCustomClassView.as_view(), name='default-context-mixin-bccv'), 18 | url(r'^default-header-context-mixin-bccv/$', views.DefaultHeaderContextMixinBetterCustomClassView.as_view(), name='default-header-context-mixin-bccv'), 19 | url(r'^default-header-mixin-json-ccv/$', views.JsonDefaultHeaderMixinCustomClassView.as_view(), name='default-header-mixin-json-ccv'), 20 | url(r'^header-prefix-bccv/$', views.HeaderPrefixBetterCustomClassView.as_view(), name='header-prefix-bccv'), 21 | url(r'^header-prefix-default-bccv/$', views.HeaderPrefixDefaultBetterCustomClassView.as_view(), name='header-prefix-default-bccv'), 22 | url(r'extra-context-12-bccv/$', views.ExtraContext12BetterCustomClassView.as_view(), name='extra-context-12-bccv'), 23 | url(r'extra-context-21-bccv/$', views.ExtraContext21BetterCustomClassView.as_view(), name='extra-context-21-bccv'), 24 | url(r'all-together-now-bccv/$', views.AllTogetherNowBetterCustomClassView.as_view(), name='all-together-now-bccv'), 25 | ] 26 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import render 5 | from django.http import HttpResponse 6 | import json 7 | 8 | from . import mixins 9 | 10 | class CustomClassView: 11 | context = [] 12 | header = '' 13 | 14 | def __init__(self, **kwargs): 15 | self.kwargs = kwargs 16 | for (k,v) in kwargs.items(): 17 | setattr(self, k, v) 18 | 19 | def render(self): 20 | print ("Custom Class View render") 21 | return """ 22 | 23 | 24 | 25 | 26 | 27 | 28 |

{header}

29 | {body} 30 | 31 | 32 | """.format( 33 | header=self.header, body='
'.join(self.context), 34 | ) 35 | 36 | @classmethod 37 | def as_view(cls, *args, **kwargs): 38 | def view(request, ): 39 | instance = cls(**kwargs) 40 | return HttpResponse(instance.render()) 41 | 42 | return view 43 | 44 | class InheritsCustomClassView(CustomClassView, ): 45 | header = "Hi" 46 | context = ['test', 'test2' ] 47 | 48 | 49 | class BetterCustomClassView(CustomClassView, ): 50 | def get_header(self, ): 51 | print ("Better Custom Class View get_header") 52 | return self.header if self.header else "" 53 | 54 | def get_context(self , ): 55 | return self.context if self.context else [] 56 | 57 | def render_context(self): 58 | context = self.get_context() 59 | if context: 60 | return '
'.join(context) 61 | return "" 62 | 63 | def render(self): 64 | print ("Better Custom Class View render") 65 | return """ 66 | 67 | 68 | 69 | 70 | 71 | 72 |

{header}

73 | {body} 74 | 75 | 76 | """.format( 77 | header=self.get_header(), body=self.render_context(), 78 | ) 79 | 80 | 81 | class DefaultHeaderBetterCustomClassView(BetterCustomClassView, ): 82 | def get_header(self, ): 83 | return self.header if self.header else "DEFAULT HEADER" 84 | 85 | class DefaultContextBetterCustomClassView(BetterCustomClassView, ): 86 | def get_context(self, ): 87 | return self.context if self.context else ["DEFAULT CONTEXT"] 88 | 89 | class JsonCustomClassView: 90 | header = '' 91 | context = [] 92 | 93 | def get_header(self, ): 94 | return self.header if self.header else "" 95 | 96 | def get_context(self, ): 97 | return self.context if self.context else [] 98 | 99 | @classmethod 100 | def as_view(cls, *args, **kwargs): 101 | def view(request, ): 102 | instance = cls(**kwargs) 103 | return HttpResponse(json.dumps({ 104 | 'header': instance.get_header(), 105 | 'context': instance.get_context(), 106 | })) 107 | 108 | return view 109 | 110 | 111 | class DefaultHeaderJsonCustomClassView(DefaultHeaderBetterCustomClassView, JsonCustomClassView): 112 | pass 113 | print (DefaultHeaderJsonCustomClassView.__mro__) 114 | class JsonDefaultHeaderCustomClassView(JsonCustomClassView, DefaultHeaderBetterCustomClassView): 115 | pass 116 | print (JsonDefaultHeaderCustomClassView.__mro__) 117 | class DefaultHeaderContextCustomClassView(DefaultHeaderBetterCustomClassView, DefaultContextBetterCustomClassView): 118 | pass 119 | print (DefaultHeaderContextCustomClassView.__mro__) 120 | 121 | class DefaultHeaderMixinBetterCustomClassView(mixins.DefaultHeaderMixin, BetterCustomClassView): 122 | pass 123 | 124 | class DefaultContextMixinBetterCustomClassView(mixins.DefaultContextMixin, BetterCustomClassView): 125 | pass 126 | 127 | class DefaultHeaderContextMixinBetterCustomClassView(mixins.DefaultHeaderMixin, mixins.DefaultContextMixin, BetterCustomClassView): 128 | pass 129 | 130 | 131 | class JsonDefaultHeaderMixinCustomClassView(mixins.DefaultHeaderMixin, JsonCustomClassView): 132 | pass 133 | 134 | class HeaderPrefixBetterCustomClassView(mixins.HeaderPrefixMixin, BetterCustomClassView): 135 | header='Hello!' 136 | 137 | class HeaderPrefixDefaultBetterCustomClassView(mixins.HeaderPrefixMixin, mixins.DefaultHeaderSuperMixin, BetterCustomClassView): 138 | pass 139 | 140 | class ExtraContext12BetterCustomClassView(mixins.ExtraContext1Mixin, mixins.ExtraContext2Mixin, BetterCustomClassView): 141 | pass 142 | 143 | class ExtraContext21BetterCustomClassView(mixins.ExtraContext2Mixin, mixins.ExtraContext1Mixin, BetterCustomClassView): 144 | pass 145 | 146 | class AllTogetherNowBetterCustomClassView( 147 | mixins.HeaderPrefixMixin, 148 | mixins.DefaultHeaderSuperMixin, 149 | mixins.ExtraContext1Mixin, 150 | mixins.ExtraContext2Mixin, 151 | BetterCustomClassView 152 | ): 153 | pass 154 | 155 | 156 | class HomeCustomClassView(mixins.UrlPatternsMixin, BetterCustomClassView, ): 157 | def get_urlpatterns(self): 158 | from core.urls import urlpatterns 159 | return urlpatterns 160 | 161 | def render_context(self): 162 | return self.render_patterns() 163 | -------------------------------------------------------------------------------- /djangocbv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/cbv-tutorial/ae6f8a1011617bb4db0fdb648a6d0ec64f73f950/djangocbv/__init__.py -------------------------------------------------------------------------------- /djangocbv/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category 4 | 5 | class CategoryAdmin(admin.ModelAdmin): 6 | pass 7 | admin.site.register(Category, CategoryAdmin) 8 | -------------------------------------------------------------------------------- /djangocbv/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangocbvConfig(AppConfig): 5 | name = 'djangocbv' 6 | -------------------------------------------------------------------------------- /djangocbv/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from .models import Article, Document 3 | 4 | class ArticleFilter(django_filters.FilterSet): 5 | class Meta: 6 | model = Article 7 | fields = { 8 | 'title': ['icontains'] 9 | } 10 | 11 | 12 | class DocumentFilter(django_filters.FilterSet): 13 | class Meta: 14 | model = Document 15 | fields = { 16 | 'description': ['icontains'] 17 | } -------------------------------------------------------------------------------- /djangocbv/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from .models import Article, Document, Category 3 | 4 | 5 | class ArticleForm(ModelForm): 6 | class Meta: 7 | model = Article 8 | fields = ['category', 'owned_by', 'title', 'content', ] 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.request = kwargs.pop('request', None) 12 | super().__init__(*args, **kwargs) 13 | 14 | if not self.request.user.has_perm('djangocbv.admin_access'): 15 | self.fields.pop('owned_by') 16 | 17 | 18 | class DocumentForm(ModelForm): 19 | class Meta: 20 | model = Document 21 | fields = ['category', 'description', 'file', 'owned_by', ] 22 | 23 | def __init__(self, *args, **kwargs): 24 | self.request = kwargs.pop('request', None) 25 | super().__init__(*args, **kwargs) 26 | 27 | if not self.request.user.has_perm('djangocbv.admin_access'): 28 | self.fields.pop('owned_by') -------------------------------------------------------------------------------- /djangocbv/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-12 22:58 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('REMOVED', 'Removed')], max_length=16)), 22 | ('created_on', models.DateTimeField(auto_now_add=True)), 23 | ('modified_on', models.DateTimeField(auto_now=True)), 24 | ('published_on', models.DateTimeField(blank=True, null=True)), 25 | ('title', models.CharField(max_length=128)), 26 | ('content', models.TextField()), 27 | ], 28 | options={ 29 | 'permissions': (('publisher_access', 'Publisher Access'), ('admin_access', 'Admin Access')), 30 | 'abstract': False, 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='Category', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('name', models.CharField(max_length=128)), 38 | ], 39 | ), 40 | migrations.CreateModel( 41 | name='Document', 42 | fields=[ 43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('PUBLISHED', 'Published'), ('REMOVED', 'Removed')], max_length=16)), 45 | ('created_on', models.DateTimeField(auto_now_add=True)), 46 | ('modified_on', models.DateTimeField(auto_now=True)), 47 | ('published_on', models.DateTimeField(blank=True, null=True)), 48 | ('description', models.CharField(max_length=128)), 49 | ('file', models.FileField(upload_to='')), 50 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='djangocbv.Category')), 51 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='document_created_by', to=settings.AUTH_USER_MODEL)), 52 | ('modified_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='document_modified_by', to=settings.AUTH_USER_MODEL)), 53 | ('owned_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='document_owned_by', to=settings.AUTH_USER_MODEL)), 54 | ], 55 | options={ 56 | 'permissions': (('publisher_access', 'Publisher Access'), ('admin_access', 'Admin Access')), 57 | 'abstract': False, 58 | }, 59 | ), 60 | migrations.AddField( 61 | model_name='article', 62 | name='category', 63 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='djangocbv.Category'), 64 | ), 65 | migrations.AddField( 66 | model_name='article', 67 | name='created_by', 68 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='article_created_by', to=settings.AUTH_USER_MODEL), 69 | ), 70 | migrations.AddField( 71 | model_name='article', 72 | name='modified_by', 73 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='article_modified_by', to=settings.AUTH_USER_MODEL), 74 | ), 75 | migrations.AddField( 76 | model_name='article', 77 | name='owned_by', 78 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='article_owned_by', to=settings.AUTH_USER_MODEL), 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /djangocbv/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spapas/cbv-tutorial/ae6f8a1011617bb4db0fdb648a6d0ec64f73f950/djangocbv/migrations/__init__.py -------------------------------------------------------------------------------- /djangocbv/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import UserPassesTestMixin 3 | from django.http import HttpResponse 4 | from django.urls import reverse 5 | from django.contrib.auth.mixins import LoginRequiredMixin 6 | import csv, json 7 | 8 | from .models import Category 9 | 10 | 11 | class AuditableMixin: 12 | def form_valid(self, form, ): 13 | if not form.instance.created_by_id: 14 | form.instance.created_by = self.request.user 15 | form.instance.modified_by = self.request.user 16 | return super().form_valid(form) 17 | 18 | 19 | class LimitAccessMixin: 20 | def get_queryset(self): 21 | qs = super().get_queryset() 22 | if self.request.user.has_perm('djangocbv.admin_access') or self.request.user.has_perm('djangocbv.publisher_access') : 23 | return qs 24 | return qs.filter(owned_by=self.request.user) 25 | 26 | 27 | class HideRemovedMixin: 28 | def get_queryset(self): 29 | qs = super().get_queryset() 30 | if self.request.user.has_perm('djangocbv.admin_access') or self.request.user.has_perm('djangocbv.publisher_access') : 31 | return qs 32 | return qs.exclude(status='REMOVED') 33 | 34 | 35 | class ModerationMixin: 36 | def form_valid(self, form): 37 | if form.instance.status != 'REMOVED': 38 | if self.request.user.has_perm('djangocbv.publisher_access'): 39 | form.instance.status = 'PUBLISHED' 40 | else: 41 | form.instance.status = 'DRAFT' 42 | 43 | return super().form_valid(form) 44 | 45 | 46 | class SetInitialMixin(object,): 47 | def get_initial(self): 48 | initial = super(SetInitialMixin, self).get_initial() 49 | initial.update(self.request.GET.dict()) 50 | return initial 51 | 52 | 53 | class SuccessMessageMixin(object, ): 54 | success_message = '' 55 | 56 | def get_success_message(self): 57 | return self.success_message 58 | 59 | def form_valid(self, form): 60 | messages.success(self.request, self.get_success_message()) 61 | return super().form_valid(form) 62 | 63 | 64 | class AnyPermissionRequiredMixin(UserPassesTestMixin): 65 | permissions = [] 66 | 67 | def test_func(self): 68 | for p in self.permissions: 69 | if self.request.user.has_perm(p): 70 | return True 71 | return False 72 | 73 | 74 | class AdminOrPublisherPermissionRequiredMixin(AnyPermissionRequiredMixin): 75 | permissions = ['djangocbv.admin_access', 'djangocbv.publisher_access'] 76 | 77 | 78 | class RequestArgMixin: 79 | def get_form_kwargs(self): 80 | kwargs = super(RequestArgMixin, self).get_form_kwargs() 81 | kwargs.update({'request': self.request}) 82 | return kwargs 83 | 84 | 85 | class CategoriesContextMixin: 86 | def get_context_data(self, **kwargs): 87 | ctx = super().get_context_data(**kwargs) 88 | ctx['categories'] = Category.objects.all() 89 | return ctx 90 | 91 | 92 | class AddFilterMixin: 93 | filter_class = None 94 | 95 | def get_context_data(self, **kwargs): 96 | ctx = super().get_context_data(**kwargs) 97 | if not self.filter_class: 98 | raise NotImplementedError("Please define filter_class when using AddFilterMixin") 99 | filter = self.filter_class(self.request.GET, queryset=self.get_queryset()) 100 | ctx['filter'] = filter 101 | ctx[self.context_object_name] = filter.qs 102 | return ctx 103 | 104 | 105 | class ExportCsvMixin: 106 | def render_to_response(self, context, **response_kwargs): 107 | if self.request.GET.get('csv'): 108 | response = HttpResponse(content_type='text/csv') 109 | response['Content-Disposition'] = 'attachment; filename="export.csv"' 110 | 111 | writer = csv.writer(response) 112 | for idx, o in enumerate(context['object_list']): 113 | if idx == 0: # Write headers 114 | writer.writerow(k for (k,v) in o.__dict__.items() if not k.startswith('_')) 115 | writer.writerow(v for (k,v) in o.__dict__.items() if not k.startswith('_')) 116 | 117 | return response 118 | return super().render_to_response(context, **response_kwargs) 119 | 120 | 121 | class JsonDetailMixin: 122 | def render_to_response(self, context, **response_kwargs): 123 | if self.request.GET.get('json'): 124 | response = HttpResponse(content_type='application/json') 125 | response.write(json.dumps(dict( (k,str(v)) for k,v in self.object.__dict__.items() ))) 126 | return response 127 | return super().render_to_response(context, **response_kwargs) 128 | 129 | 130 | class SetOwnerIfNeeded: 131 | def form_valid(self, form, ): 132 | if not form.instance.owned_by_id: 133 | form.instance.owned_by = self.request.user 134 | return super().form_valid(form) 135 | 136 | 137 | class ChangeStatusMixin: 138 | new_status = None 139 | 140 | def form_valid(self, form, ): 141 | if not self.new_status: 142 | raise NotImplementedError("Please define new_status when using ChangeStatusMixin") 143 | form.instance.status = self.new_status 144 | return super().form_valid(form) 145 | 146 | 147 | class ContentCreateMixin(SuccessMessageMixin, 148 | AuditableMixin, 149 | SetOwnerIfNeeded, 150 | RequestArgMixin, 151 | SetInitialMixin, 152 | ModerationMixin, 153 | LoginRequiredMixin): 154 | success_message = 'Object successfully created!' 155 | 156 | 157 | class ContentUpdateMixin(SuccessMessageMixin, 158 | AuditableMixin, 159 | SetOwnerIfNeeded, 160 | RequestArgMixin, 161 | SetInitialMixin, 162 | ModerationMixin, 163 | LimitAccessMixin, 164 | LoginRequiredMixin): 165 | success_message = 'Object successfully updated!' 166 | 167 | 168 | class ContentListMixin(ExportCsvMixin, AddFilterMixin, HideRemovedMixin, ): 169 | pass 170 | 171 | 172 | class ContentRemoveMixin(SuccessMessageMixin, 173 | AdminOrPublisherPermissionRequiredMixin, 174 | AuditableMixin, 175 | ChangeStatusMixin,): 176 | http_method_names = ['post',] 177 | new_status = 'REMOVED' 178 | fields = [] 179 | success_message = 'Object successfully removed!' 180 | 181 | 182 | class ContentUnpublishMixin(SuccessMessageMixin, 183 | AdminOrPublisherPermissionRequiredMixin, 184 | AuditableMixin, 185 | ChangeStatusMixin,): 186 | http_method_names = ['post',] 187 | new_status = 'DRAFT' 188 | fields = [] 189 | success_message = 'Object successfully unpublished!' 190 | -------------------------------------------------------------------------------- /djangocbv/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | 5 | STATUS_CHOICES = ( 6 | ('DRAFT', 'Draft', ), 7 | ('PUBLISHED', 'Published', ), 8 | ('REMOVED', 'Removed', ), 9 | ) 10 | 11 | 12 | class Category(models.Model): 13 | name = models.CharField(max_length=128, ) 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | class Meta: 19 | permissions = ( 20 | ("publisher_access", "Publisher Access"), 21 | ("admin_access", "Admin Access"), 22 | ) 23 | 24 | 25 | class AbstractGeneralInfo(models.Model): 26 | status = models.CharField(max_length=16, choices=STATUS_CHOICES, ) 27 | category = models.ForeignKey('category', on_delete=models.PROTECT, ) 28 | created_on = models.DateTimeField(auto_now_add=True, ) 29 | created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='%(class)s_created_by', ) 30 | modified_on = models.DateTimeField(auto_now=True, ) 31 | modified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='%(class)s_modified_by', ) 32 | 33 | owned_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='%(class)s_owned_by', ) 34 | published_on = models.DateTimeField(blank=True, null=True) 35 | 36 | class Meta: 37 | abstract = True 38 | 39 | 40 | class Article(AbstractGeneralInfo): 41 | title = models.CharField(max_length=128, ) 42 | content = models.TextField() 43 | 44 | 45 | class Document(AbstractGeneralInfo): 46 | description = models.CharField(max_length=128, ) 47 | file = models.FileField() 48 | -------------------------------------------------------------------------------- /djangocbv/templates/_categories_partial.html: -------------------------------------------------------------------------------- 1 | List of categories: {% for cat in categories %}{{ cat }}{% if not forloop.last %}|{% endif %}{% endfor %} -------------------------------------------------------------------------------- /djangocbv/templates/django_cbv_home.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Home{% endblock %} 4 | {% block content %} 5 | Welcome ! 6 | 7 | You can visit: 8 | 9 | 15 | 16 | {% include "_categories_partial.html" %} 17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/_article_content_partial.html: -------------------------------------------------------------------------------- 1 | {{ article.content|linebreaks }} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Article detail{% endblock %} 4 | {% block content %} 5 | 6 |

{{ article.title }}

7 | 8 | {{ article.status }} 9 |
10 | {{ article.owned_by }} {% if published %} at {{ article.published_on}} {% endif %} 11 | 12 | {% include "djangocbv/_article_content_partial.html" %} 13 | 14 | 15 | 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/article_form.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Article form{% endblock %} 4 | {% block content %} 5 | 6 |
7 | {{ form }} 8 | {% csrf_token %} 9 | 10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/article_list.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Article list{% endblock %} 4 | {% block extra_style%} 5 | 29 | {% endblock %} 30 | {% block content %} 31 | 32 |

33 |

34 | {{ filter.form }} 35 | 36 |
37 | Export csv 38 |

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {% for article in articles %} 54 | 55 | 56 | 57 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | {% endfor %} 77 | 78 |
IdControlTitleStatusContentCategoryOwner
{{ article.id }} 58 | View 59 | Edit 60 | {% if article.status == 'DRAFT' %} 61 |
{% csrf_token %}
62 | {% endif %} 63 | {% if article.status != 'DRAFT' %} 64 |
{% csrf_token %}
65 | {% endif %} 66 |
{{ article.title }}{{ article.get_status_display }} 70 | {{ article.content|truncatewords:20 }} 71 | 72 | {{ article.category }}{{ article.owned_by }}
79 | 80 | {% endblock %} 81 | 82 | {% block extra_script %} 83 | 94 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/category_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Category detail{% endblock %} 4 | {% block content %} 5 | 6 |

{{ category.name }}

7 | 8 |

9 | Article number: {{ article_number }}
10 | Document number: {{ document_number }} 11 |

12 | 13 | {% include "_categories_partial.html" %} 14 | 15 | 16 | 17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/category_form.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Category form{% endblock %} 4 | {% block content %} 5 | 6 |
7 | {{ form }} 8 | {% csrf_token %} 9 | 10 |
11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Category list{% endblock %} 4 | {% block content %} 5 |

6 | Export csv 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for cat in categories %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | {% endfor %} 36 | 37 |
EditTitle# articles# documentsAdd
{{ cat.id }}{{ cat.name }}{{ cat.article_cnt }}{{ cat.document_cnt }} 30 | Article 31 | | 32 | Document 33 |
38 | 39 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/document_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Document detail{% endblock %} 4 | {% block content %} 5 | 6 |

{{ document.description }}

7 | 8 | {{ document.status }} 9 |
10 | {{ document.owned_by }} {% if published %} at {{ document.published_on}} {% endif %} 11 |

12 | Download 13 |

14 | 15 | 16 | 17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/document_form.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Document form{% endblock %} 4 | {% block content %} 5 | 6 |
7 | {{ form }} 8 | {% csrf_token %} 9 |
10 | 11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/templates/djangocbv/document_list.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Document list{% endblock %} 4 | {% block extra_style%} 5 | 27 | {% endblock %} 28 | {% block content %} 29 | 30 |

31 |

32 | {{ filter.form }} 33 | 34 |
35 | Export csv 36 |

37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% for doc in documents %} 52 | 53 | 54 | 55 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {% endfor %} 73 | 74 |
IdControlTitleStatusCategoryOwnerFile
{{ doc.id }} 56 | View 57 | Edit 58 | {% if doc.status == 'DRAFT' %} 59 |
{% csrf_token %}
60 | {% endif %} 61 | 62 | {% if doc.status != 'DRAFT' %} 63 |
{% csrf_token %}
64 | {% endif %} 65 |
{{ doc.description }}{{ doc.get_status_display }}{{ doc.category }}{{ doc.owned_by }}{{ doc.file }}
75 | 76 | {% endblock %} 77 | 78 | {% block extra_script %} 79 | 90 | {% endblock %} -------------------------------------------------------------------------------- /djangocbv/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /djangocbv/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.urls import path, re_path 3 | from django.contrib import admin 4 | 5 | from . import views 6 | 7 | urlpatterns = [ 8 | path('', views.DjangoHomeCustomClassView.as_view(), name='home_django'), 9 | path('django_better_ccv/', views.DjangoBetterCustomClassView.as_view(), name='django_better_ccv'), 10 | path('django_header_context_better_ccv/', views.DefaultHeaderContextDjangoBetterCustomClassView.as_view(), name='django_header_context_better_ccv'), 11 | 12 | path('articles/', views.ArticleListView.as_view(), name='article-list'), 13 | path('articles/create/', views.ArticleCreateView.as_view(), name='article-create'), 14 | path('articles/update//', views.ArticleUpdateView.as_view(), name='article-update'), 15 | path('articles/detail//', views.ArticleDetailView.as_view(), name='article-detail'), 16 | path('articles/remove//', views.ArticleRemoveView.as_view(), name='article-remove'), 17 | path('articles/unpublish//', views.ArticleUnpublishView.as_view(), name='article-unpublish'), 18 | 19 | path('documents/', views.DocumentListView.as_view(), name='document-list'), 20 | path('documents/create/', views.DocumentCreateView.as_view(), name='document-create'), 21 | path('documents/update//', views.DocumentUpdateView.as_view(), name='document-update'), 22 | path('documents/detail//', views.DocumentDetailView.as_view(), name='document-detail'), 23 | path('documents/remove//', views.DocumentRemoveView.as_view(), name='document-remove'), 24 | path('documents/unpublish//', views.DocumentUnpublishView.as_view(), name='document-unpublish'), 25 | 26 | path('categories/', views.CategoryListView.as_view(), name='category-list'), 27 | path('categories/create/', views.CategoryCreateView.as_view(), name='category-create'), 28 | path('categories/detail//', views.CategoryDetailView.as_view(), name='category-detail'), 29 | path('categories/update//', views.CategoryUpdateView.as_view(), name='category-update'), 30 | 31 | re_path(r'^show/(?Phelp|about)/', views.DynamicTemplateView.as_view(), name='template-show'), 32 | 33 | ] 34 | -------------------------------------------------------------------------------- /djangocbv/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db.models import Count 5 | from django.shortcuts import render 6 | from django.http import HttpResponse, Http404 7 | from django.views.generic import TemplateView, ListView, CreateView, UpdateView, DetailView 8 | from django.views.generic.base import View 9 | from django.urls import reverse_lazy 10 | 11 | from core.views import HomeCustomClassView 12 | 13 | from core.mixins import DefaultHeaderMixin, DefaultContextMixin, UrlPatternsMixin 14 | 15 | from .models import Article, Category, Document 16 | from .forms import ArticleForm, DocumentForm 17 | from .filters import ArticleFilter, DocumentFilter 18 | from .mixins import * 19 | 20 | 21 | class DjangoHomeCustomClassView(UrlPatternsMixin, CategoriesContextMixin, TemplateView, ): 22 | template_name = 'django_cbv_home.html' 23 | 24 | def get_urlpatterns(self): 25 | from djangocbv.urls import urlpatterns 26 | 27 | return [ p for p in urlpatterns if ':' not in p.pattern.describe() and '<' not in p.pattern.describe() ] 28 | 29 | def get_context_data(self, **kwargs): 30 | context = super().get_context_data(**kwargs) 31 | context['urls'] = self.render_patterns() 32 | return context 33 | 34 | 35 | class DjangoBetterCustomClassView(View, ): 36 | header = '' 37 | context ='' 38 | 39 | def get_header(self, ): 40 | return self.header if self.header else "" 41 | 42 | def get_context(self , ): 43 | return self.context if self.context else [] 44 | 45 | def render_context(self): 46 | context = self.get_context() 47 | if context: 48 | return '
'.join(context) 49 | return "" 50 | 51 | def get(self, *args, **kwargs): 52 | resp = """ 53 | 54 | 55 | 56 | 57 | 58 | 59 |

{header}

60 | {body} 61 | 62 | 63 | """.format( 64 | header=self.get_header(), body=self.render_context(), 65 | ) 66 | return HttpResponse(resp) 67 | 68 | 69 | class DefaultHeaderContextDjangoBetterCustomClassView(DefaultHeaderMixin, DefaultContextMixin, DjangoBetterCustomClassView): 70 | pass 71 | 72 | 73 | class CategoryListView(ExportCsvMixin, AdminOrPublisherPermissionRequiredMixin, ListView): 74 | model = Category 75 | context_object_name = 'categories' 76 | 77 | def get_queryset(self): 78 | qs = super().get_queryset() 79 | return qs.annotate(article_cnt=Count('article'), document_cnt=Count('document')) 80 | 81 | 82 | class CategoryCreateView(SuccessMessageMixin, AdminOrPublisherPermissionRequiredMixin, CreateView): 83 | model = Category 84 | fields = ['name'] 85 | success_message = 'Category created!' 86 | success_url = reverse_lazy('category-list') 87 | 88 | 89 | class CategoryUpdateView(SuccessMessageMixin, AdminOrPublisherPermissionRequiredMixin, UpdateView): 90 | model = Category 91 | fields = ['name'] 92 | success_message = 'Category updated!' 93 | success_url = reverse_lazy('category-list') 94 | 95 | 96 | class CategoryDetailView(CategoriesContextMixin, DetailView): 97 | model = Category 98 | context_object_name = 'category' 99 | 100 | def get_context_data(self, **kwargs): 101 | ctx = super().get_context_data(**kwargs) 102 | ctx['article_number'] = Article.objects.filter(category=self.object).count() 103 | ctx['document_number'] = Document.objects.filter(category=self.object).count() 104 | return ctx 105 | 106 | 107 | class ArticleListView(ContentListMixin, ListView): 108 | model = Article 109 | context_object_name = 'articles' 110 | filter_class = ArticleFilter 111 | 112 | 113 | class ArticleCreateView(ContentCreateMixin, CreateView): 114 | model = Article 115 | form_class = ArticleForm 116 | success_url = reverse_lazy('article-list') 117 | 118 | 119 | class ArticleUpdateView(ContentUpdateMixin, UpdateView): 120 | model = Article 121 | form_class = ArticleForm 122 | success_url = reverse_lazy('article-list') 123 | 124 | 125 | class ArticleDetailView(HideRemovedMixin, JsonDetailMixin, DetailView): 126 | model = Article 127 | context_object_name = 'article' 128 | 129 | def get_template_names(self): 130 | if self.request.is_ajax() or self.request.GET.get('partial'): 131 | return 'djangocbv/_article_content_partial.html' 132 | return super().get_template_names() 133 | 134 | 135 | class ArticleRemoveView(ContentRemoveMixin, UpdateView): 136 | model = Article 137 | success_url = reverse_lazy('article-list') 138 | 139 | 140 | class ArticleUnpublishView(ContentUnpublishMixin, UpdateView): 141 | model = Article 142 | success_url = reverse_lazy('article-list') 143 | 144 | 145 | class DocumentListView(ContentListMixin, ListView): 146 | model = Document 147 | context_object_name = 'documents' 148 | filter_class = DocumentFilter 149 | 150 | 151 | class DocumentCreateView(ContentCreateMixin, CreateView): 152 | model = Document 153 | form_class = DocumentForm 154 | success_url = reverse_lazy('document-list') 155 | 156 | 157 | class DocumentUpdateView(ContentUpdateMixin, UpdateView): 158 | model = Document 159 | form_class = DocumentForm 160 | success_url = reverse_lazy('document-list') 161 | 162 | 163 | class DocumentDetailView(HideRemovedMixin, JsonDetailMixin, DetailView): 164 | model = Document 165 | context_object_name = 'document' 166 | 167 | 168 | class DocumentRemoveView(ContentRemoveMixin, UpdateView): 169 | model = Document 170 | success_url = reverse_lazy('document-list') 171 | 172 | 173 | class DocumentUnpublishView(ContentUnpublishMixin, UpdateView): 174 | model = Document 175 | success_url = reverse_lazy('document-list') 176 | 177 | 178 | class DynamicTemplateView(TemplateView): 179 | def get_template_names(self): 180 | what = self.kwargs['what'] 181 | return '{0}.html'.format(what) 182 | -------------------------------------------------------------------------------- /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", "cbv_tutorial.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.0.9 2 | django-extensions==2.0.5 3 | pytz==2018.3 4 | six==1.11.0 5 | Werkzeug==0.14.1 6 | -------------------------------------------------------------------------------- /rsp.bat: -------------------------------------------------------------------------------- 1 | set WERKZEUG_DEBUG_PIN=off 2 | python manage.py runserver_plus %* 3 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | python manage.py runserver %* 2 | -------------------------------------------------------------------------------- /templates/_partial_account.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/_partial_messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 |
    3 | {% for message in messages %} 4 | {{ message }} 5 | {% endfor %} 6 |
7 | {% endif %} -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}About{% endblock %} 4 | {% block content %} 5 | About page ... 6 | {% endblock %} -------------------------------------------------------------------------------- /templates/help.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Help{% endblock %} 4 | {% block content %} 5 | Help page ... 6 | {% endblock %} -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title%}Home{% endblock %} 4 | {% block content %} 5 | Welcome ! 6 | 7 | You can visit: 8 | 9 | 13 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title %}Logout{% endblock %} 4 | 5 | {% block content %} 6 | Goodbye! 7 | {% endblock %} -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "site_base.html" %} 2 | 3 | {% block title %}Login{% endblock %} 4 | 5 | {% block content %} 6 | 7 | {% if form.errors %} 8 |

Your username and password didn't match. Please try again.

9 | {% endif %} 10 | 11 | {% if next %} 12 | {% if user.is_authenticated %} 13 |

Your account doesn't have access to this page. To proceed, 14 | please login with an account that has access.

15 | {% else %} 16 |

Please login to see this page.

17 | {% endif %} 18 | {% endif %} 19 | 20 |
21 | {% csrf_token %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
32 | 33 | 34 | 35 |
36 | {% endblock %} -------------------------------------------------------------------------------- /templates/site_base.html: -------------------------------------------------------------------------------- 1 | {% load custom_tags %} 2 | 3 | 4 | 5 | 6 | 36 | {% block extra_style %} 37 | {% endblock %} 38 | 39 | 40 |
41 |

{% block title %}{% endblock %}

42 | Home 43 | | 44 | Help 45 | | 46 | About 47 | {% include '_partial_account.html' %} 48 |
49 | {% include '_partial_messages.html' %} 50 | 51 |
52 | {% block content %} 53 | {% endblock %} 54 | 59 | {% block extra_script %} 60 | {% endblock %} 61 | 62 | --------------------------------------------------------------------------------