Unverified Kaydet (Commit) 1d21a00f authored tarafından Uğur Özyılmazel's avatar Uğur Özyılmazel

Fix: default managers for base models

üst 44503153
...@@ -251,7 +251,7 @@ ignore-on-opaque-inference=yes ...@@ -251,7 +251,7 @@ ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful # List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of # for classes with dynamically set attributes). This supports the use of
# qualified names. # qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,__proxy__,Style,BaseModelWithSoftDelete ignored-classes=optparse.Values,thread._local,_thread._local,__proxy__,Style,BaseModelWithSoftDelete,BasicPost
# List of module names for which member attributes should not be checked # List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime # (useful for modules/projects where namespaces are manipulated during runtime
......
[![Build Status](https://travis-ci.org/vigo/django2-project-template.svg?branch=master)](https://travis-ci.org/vigo/django2-project-template) [![Build Status](https://travis-ci.org/vigo/django2-project-template.svg?branch=master)](https://travis-ci.org/vigo/django2-project-template)
![Python](https://img.shields.io/badge/django-3.7.0-green.svg) ![Python](https://img.shields.io/badge/django-3.7.0-green.svg)
![Django](https://img.shields.io/badge/django-2.1.3-green.svg) ![Django](https://img.shields.io/badge/django-2.1.3-green.svg)
![Version](https://img.shields.io/badge/version-1.0.1-yellow.svg) ![Version](https://img.shields.io/badge/version-1.1.0-yellow.svg)
# Django Project Starter Template # Django Project Starter Template
...@@ -760,7 +760,9 @@ This is a common model. By default, `BaseModel` contains these fields: ...@@ -760,7 +760,9 @@ This is a common model. By default, `BaseModel` contains these fields:
- `updated_at` - `updated_at`
- `status` - `status`
Also has custom manager called: `objects_bm`. There are 4 basic status types: We are overriding the default manager. `BaseModel` uses `BaseModelQuerySet` as
manager, `BaseModelWithSoftDelete` uses `BaseModelWithSoftDeleteManager`.
There are 4 basic status types:
```python ```python
STATUS_OFFLINE = 0 STATUS_OFFLINE = 0
...@@ -769,13 +771,13 @@ STATUS_DELETED = 2 ...@@ -769,13 +771,13 @@ STATUS_DELETED = 2
STATUS_DRAFT = 3 STATUS_DRAFT = 3
``` ```
Custom manager has custom querysets against these statuses such as: You can make these queries:
```python ```python
>>> Post.objects_bm.deleted() # filters: status = STATUS_DELETED >>> Post.objects.deleted() # filters: status = STATUS_DELETED
>>> Post.objects_bm.actives() # filters: status = STATUS_ONLINE >>> Post.objects.actives() # filters: status = STATUS_ONLINE
>>> Post.objects_bm.offlines() # filters: status = STATUS_OFFLINE >>> Post.objects.offlines() # filters: status = STATUS_OFFLINE
>>> Post.objects_bm.drafts() # filters: status = STATUS_DRAFT >>> Post.objects.drafts() # filters: status = STATUS_DRAFT
``` ```
## `BaseModelWithSoftDelete` ## `BaseModelWithSoftDelete`
...@@ -794,7 +796,7 @@ This works exactly like Django’s `delete()`. Broadcasts `pre_delete` and ...@@ -794,7 +796,7 @@ This works exactly like Django’s `delete()`. Broadcasts `pre_delete` and
a dictionary with the number of deletion-marks per object type. a dictionary with the number of deletion-marks per object type.
```python ```python
>>> Post.objects_bm.all() >>> Post.objects.all()
SELECT "blog_post"."id", SELECT "blog_post"."id",
"blog_post"."created_at", "blog_post"."created_at",
...@@ -812,7 +814,7 @@ Execution time: 0.000135s [Database: default] ...@@ -812,7 +814,7 @@ Execution time: 0.000135s [Database: default]
<BaseModelWithSoftDeleteQuerySet [<Post: Python post 1>, <Post: Python post 2>, <Post: Python post 3>]> <BaseModelWithSoftDeleteQuerySet [<Post: Python post 1>, <Post: Python post 2>, <Post: Python post 3>]>
>>> Category.objects_bm.all() >>> Category.objects.all()
SELECT "blog_category"."id", SELECT "blog_category"."id",
"blog_category"."created_at", "blog_category"."created_at",
...@@ -826,14 +828,14 @@ SELECT "blog_category"."id", ...@@ -826,14 +828,14 @@ SELECT "blog_category"."id",
<BaseModelWithSoftDeleteQuerySet [<Category: Python>]> <BaseModelWithSoftDeleteQuerySet [<Category: Python>]>
>>> Category.objects_bm.delete() >>> Category.objects.delete()
(4, {'blog.Category': 1, 'blog.Post': 3}) (4, {'blog.Category': 1, 'blog.Post': 3})
>>> Category.objects_bm.all() >>> Category.objects.all()
<BaseModelWithSoftDeleteQuerySet []> # rows are still there! don’t panic! <BaseModelWithSoftDeleteQuerySet []> # rows are still there! don’t panic!
>>> Category.objects.all() >>> Category.objects.deleted()
<QuerySet [<Category: Python>]> <BaseModelWithSoftDeleteQuerySet [<Category: Python>]>
``` ```
...@@ -1406,46 +1408,7 @@ $ rake test:coverage ...@@ -1406,46 +1408,7 @@ $ rake test:coverage
```bash ```bash
$ DJANGO_ENV=test python manage.py test baseapp -v 2 # or $ DJANGO_ENV=test python manage.py test baseapp -v 2 # or
$ DJANGO_ENV=test python manage.py test baseapp.tests.test_user.CustomUserTestCase # run single unit $ DJANGO_ENV=test python manage.py test baseapp.tests.test_user.CustomUserTestCase # run single unit
$ rake test:baseapp $ rake test:run[baseapp]
```
---
## Notes
If you created models via management command or rake task, you’ll have admin
file automatically and generated against your model type. If you created a model
with `BaseModelWithSoftDelete`, you’ll have `BaseAdminWithSoftDelete` set.
`BaseAdminWithSoftDelete` uses `objects_bm` in `get_queryset` and by default,
you’ll have extra actions and soft delete feature. If you don’t want to use
`objects_bm` manager, you need to override it manually:
```python
# example: blog/admin/post.py
from django.contrib import admin
from baseapp.admin import BaseAdminWithSoftDelete
from ..models import Post
__all__ = [
'PostAdmin',
]
class PostAdmin(BaseAdminWithSoftDelete):
# sticky_list_filter = None
# hide_deleted_at = False
def get_queryset(self, request):
return self.model.objects.get_queryset() # this line!
admin.site.register(Post, PostAdmin)
``` ```
--- ---
...@@ -1504,6 +1467,11 @@ This project is licensed under MIT ...@@ -1504,6 +1467,11 @@ This project is licensed under MIT
## Change Log ## Change Log
**2018-12-04**
- Update: `BaseModel` and `BaseModelWithSoftDelete` now override `objects` via
their own managers.
**2018-11-30** **2018-11-30**
- Update: flake8 ignores configured - Update: flake8 ignores configured
......
...@@ -42,7 +42,7 @@ class BaseAdminWithSoftDelete(BaseAdmin): ...@@ -42,7 +42,7 @@ class BaseAdminWithSoftDelete(BaseAdmin):
hide_deleted_at = True hide_deleted_at = True
def get_queryset(self, request): def get_queryset(self, request):
qs = self.model.objects_bm.get_queryset() qs = self.model.objects.get_queryset()
if request.GET.get('status__exact', None): if request.GET.get('status__exact', None):
if ( if (
numerify(request.GET.get('status__exact')) numerify(request.GET.get('status__exact'))
......
from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import ( from django.contrib.auth.admin import (
UserAdmin as BaseUserAdmin, UserAdmin as BaseUserAdmin,
) )
from django.contrib.auth.forms import (
ReadOnlyPasswordHashField,
)
from django.db import models from django.db import models
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from ..forms import UserChangeForm, UserCreationForm
from ..models import User from ..models import User
from ..widgets import AdminImageFileWidget from ..widgets import AdminImageFileWidget
__all__ = ['UserAdmin'] __all__ = ['UserAdmin']
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
label=_('Password'),
help_text=_(
'Raw passwords are not stored, so there is no way to see this '
'user\'s password, but you can change the password using '
'<a href="../password/">this form</a>.'
),
)
class Meta:
model = User
fields = (
'email',
'first_name',
'last_name',
'password',
'is_active',
'is_staff',
'is_superuser',
)
labels = {
'first_name': _('first name').title(),
'last_name': _('last name').title(),
}
def clean_password(self):
return self.initial['password']
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(
label=_('Password'), widget=forms.PasswordInput
)
password2 = forms.CharField(
label=_('Password confirmation'),
widget=forms.PasswordInput,
)
class Meta:
"""
`fields` property holds only required fields.
"""
model = User
fields = ('first_name', 'last_name')
labels = {
'first_name': _('first name').title(),
'last_name': _('last name').title(),
}
def clean_password2(self):
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if (
password1
and password2
and password1 != password2
):
raise forms.ValidationError(
_('Passwords don\'t match')
)
return password2
def save(self, commit=True):
user = super(UserCreationForm, self).save(
commit=False
)
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
form = UserChangeForm form = UserChangeForm
......
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
ReadOnlyPasswordHashField,
)
from django.utils.translation import ugettext_lazy as _
__all__ = ['UserChangeForm', 'UserCreationForm']
User = get_user_model()
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
label=_('Password'),
help_text=_(
'Raw passwords are not stored, so there is no way to see this '
'user\'s password, but you can change the password using '
'<a href="../password/">this form</a>.'
),
)
class Meta:
model = User
fields = (
'email',
'first_name',
'last_name',
'password',
'is_active',
'is_staff',
'is_superuser',
)
labels = {
'first_name': _('first name').title(),
'last_name': _('last name').title(),
}
def clean_password(self):
return self.initial['password']
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(
label=_('Password'), widget=forms.PasswordInput
)
password2 = forms.CharField(
label=_('Password confirmation'),
widget=forms.PasswordInput,
)
class Meta:
"""
`fields` property holds only required fields.
"""
model = User
fields = ('first_name', 'last_name')
labels = {
'first_name': _('first name').title(),
'last_name': _('last name').title(),
}
def clean_password2(self):
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if (
password1
and password2
and password1 != password2
):
raise forms.ValidationError(
_('Passwords don\'t match')
)
return password2
def save(self, commit=True):
user = super(UserCreationForm, self).save(
commit=False
)
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user
...@@ -160,8 +160,7 @@ class BaseModel(models.Model): ...@@ -160,8 +160,7 @@ class BaseModel(models.Model):
verbose_name=_('Status'), verbose_name=_('Status'),
) )
objects = models.Manager() objects = BaseModelQuerySet.as_manager()
objects_bm = BaseModelQuerySet.as_manager()
class Meta: class Meta:
abstract = True abstract = True
...@@ -173,8 +172,7 @@ class BaseModelWithSoftDelete(BaseModel): ...@@ -173,8 +172,7 @@ class BaseModelWithSoftDelete(BaseModel):
null=True, blank=True, verbose_name=_('Deleted At') null=True, blank=True, verbose_name=_('Deleted At')
) )
objects = models.Manager() objects = BaseModelWithSoftDeleteManager()
objects_bm = BaseModelWithSoftDeleteManager()
class Meta: class Meta:
abstract = True abstract = True
......
...@@ -30,7 +30,7 @@ class BaseModelTestCase(TestCase): ...@@ -30,7 +30,7 @@ class BaseModelTestCase(TestCase):
def test_basemodel_queryset(self): def test_basemodel_queryset(self):
self.assertQuerysetEqual( self.assertQuerysetEqual(
BasicPost.objects_bm.all().order_by('id'), BasicPost.objects.all().order_by('id'),
[ [
'<BasicPost: Test Post 1>', '<BasicPost: Test Post 1>',
'<BasicPost: Test Post 2>', '<BasicPost: Test Post 2>',
...@@ -39,18 +39,18 @@ class BaseModelTestCase(TestCase): ...@@ -39,18 +39,18 @@ class BaseModelTestCase(TestCase):
], ],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
BasicPost.objects_bm.actives().order_by('id'), BasicPost.objects.actives().order_by('id'),
['<BasicPost: Test Post 1>'], ['<BasicPost: Test Post 1>'],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
BasicPost.objects_bm.offlines(), BasicPost.objects.offlines(),
['<BasicPost: Test Post 3>'], ['<BasicPost: Test Post 3>'],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
BasicPost.objects_bm.deleted(), BasicPost.objects.deleted(),
['<BasicPost: Test Post 2>'], ['<BasicPost: Test Post 2>'],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
BasicPost.objects_bm.drafts(), BasicPost.objects.drafts(),
['<BasicPost: Test Post 4>'], ['<BasicPost: Test Post 4>'],
) )
...@@ -36,17 +36,17 @@ class BaseModelWithSoftDeleteTestCase(TestCase): ...@@ -36,17 +36,17 @@ class BaseModelWithSoftDeleteTestCase(TestCase):
], ],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
Category.objects_bm.actives(), Category.objects.actives(),
['<Category: Python>'], ['<Category: Python>'],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
Category.objects_bm.offlines(), [] Category.objects.offlines(), []
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
Category.objects_bm.deleted(), [] Category.objects.deleted(), []
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
Category.objects_bm.drafts(), [] Category.objects.drafts(), []
) )
def test_softdelete(self): def test_softdelete(self):
...@@ -56,14 +56,11 @@ class BaseModelWithSoftDeleteTestCase(TestCase): ...@@ -56,14 +56,11 @@ class BaseModelWithSoftDeleteTestCase(TestCase):
(3, {'baseapp.Category': 1, 'baseapp.Post': 2}), (3, {'baseapp.Category': 1, 'baseapp.Post': 2}),
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
Category.objects_bm.deleted(), Category.objects.deleted(),
['<Category: Python>'], ['<Category: Python>'],
) )
self.assertQuerysetEqual( self.assertQuerysetEqual(
Category.objects.all(), ['<Category: Python>'] Post.objects.deleted().order_by('id'),
)
self.assertQuerysetEqual(
Post.objects_bm.deleted().order_by('id'),
[ [
'<Post: Python post 1>', '<Post: Python post 1>',
'<Post: Python post 2>', '<Post: Python post 2>',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment