I have an ndb.Model
class BaseModel(ndb.Model):
created_on = ndb.DateTimeProperty(auto_now_add=True)
created_by = ndb.KeyProperty(kind='MyUser') # ????
I'm looking for an elegant way create an auto "created_by" field that is populated with g.current_user key upon creation, so that
mymodel = BaseModel()
already have created_on and created_by
any ideas ?
I would recommend defining your own property class:
class OwnerProperty(ndb.KeyProperty):
_auto_current_user_add = False
def __init__(self, auto_current_user_add=False):
super(OwnerProperty, self).__init__(kind='MyUser')
self._auto_current_user_add = auto_current_user_add
def _prepare_for_put(self, entity):
if self._auto_current_user_add and not self._has_value(entity):
value = g.current_user.key
if value is not None:
self._store_value(entity, value)
super(OwnerProperty, self)._prepare_for_put(entity)
I'm not really sure how your g.current_user is setup, but assuming it is an Entity this would work. Note that like ndb.DateTimeProperty, the owner would only get populated once the entity is put for the first time.
This is based on how ndb.UserProperty is written. However, I recommend against using UserProperty directly since user properties do a lot of strange things so it is always better to handle your own user entities.
ended up doing something simple:
class BaseModel(ndb.Model):
created_on = ndb.DateTimeProperty(auto_now_add=True)
created_by = ndb.KeyProperty(kind='TalknetUser')
def __init__(self, *args, **kwargs):
super(BaseModel, self).__init__(parent=talknet_key(), *args, **kwargs)
try:
self.created_by = current_user.ndb.key
except Exception as x:
self.created_by = None
but so far it works
Related
I have the following model that I want to import:
class Token(models.Model):
key = models.CharField(db_index=True,unique=True,primary_key=True, )
pool = models.ForeignKey(Pool, on_delete=models.CASCADE)
state = models.PositiveSmallIntegerField(default=State.VALID, choices=State.choices)
then a resource model:
class TokenResource(resources.ModelResource):
class Meta:
model = Token
import_id_fields = ("key",)
and a ImportForm for querying the pool:
class AccessTokenImportForm(ImportForm):
pool = forms.ModelChoiceField(queryset=Pool.objects.all(), required=True)
This value shouls be set for all the imported token objects.
The problem is, that I did not find a way to acomplish this yet.
How do I get the value from the form to the instance?
The before_save_instance and or similar methods I cannot access these values anymore. I have to pass this alot earlier I guess. Does someone ever done something similar?
Thanks and regards
Matt
It seems that you could pass the extra value in the Admin:
class TokenAdmin(ImportMixin, admin.ModelAdmin):
def get_import_data_kwargs(self, request, *args, **kwargs):
form = kwargs.get('form')
if form:
kwargs.pop('form')
if 'pool' in form.cleaned_data:
return {'pool_id' : form.cleaned_data['pool'].id }
return kwargs
return {}
And then use this data within your resources after_import_instance method to set this value
def after_import_instance(self, instance, new, row_number=None, **kwargs):
pool_id = kwargs.get('pool_id')
instance.pool_id = pool_id
instance.created_at = timezone.now()
Then the error from missing field (non null) is gone.
Consider my models.py,
PowerPolicy:
class PowerPolicy(models.Model):
name = models.CharField(max_length=15)
...
Group:
class Group(models.Model):
name = models.CharField(max_length=15)
#But then, we also have:
power_policies = models.ManytoManyField(PowerPolicy)
Player:
class Player(models.Model):
name = models.CharField(max_length=15)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
...
And then another model called,
UsePower:
class UserPower(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE)
power_policy = models.ForeignKey(PowerPolicy, on_delete=models.CASCADE)
...
But! Here's the catch: I want to make it so that my superuser (Note that my superuser isn't a player, he's simply a superuser) can only create a UsePower object of the Powers specified in the Player's Group. Now, I do know that I have to create a custom form and override the queryset of the power_policy field that returns, my the custom queryset according to my needs through a function.
- Here's what it would look something like:
class UsePowerForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UsePowerForm, self).__init__(*args, **kwargs)
def MyCustomFunctionThatReturnsTheQuerySet():
This function returns the Power policies that are allowed to the player in
their player Group. The only problem is,
Our little function here doesn't know how to get the player chosen.
could you help
return TheQuerySet
self.fields['power_policy'].queryset = MyCustomFunctionThatReturnsTheQuerySet()
And then use it on the Admin Site, by doing this:
class UsePowerAdmin(admin.ModelAdmin):
form = UsePowerForm
admin.site.register(UsePower, UsePowerForm)
I really hope this makes sense, and you guys could help me out.
Thank you for your time reading this, I honestly do appreciate it.
EDIT: Using form cleaning, or verifying during save, is not an option for me :(
You can get the player when the form is being initialized:
class UserPowerForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UsePowerForm, self).__init__(*args, **kwargs)
player = Player.objects.get(id=self.initial['player'])
###from here you can use player to get the power policies and put into list
self.fields['power_policy'] = forms.ChoiceField(choices=power_policy_list)
class Meta:
model = UserPower
fields = ['player', 'power_policy']
I'm experiencing some major performing issue with my django admin. Lots of duplicate queries based on how many inlines that I have.
models.py
class Setting(models.Model):
name = models.CharField(max_length=50, unique=True)
class Meta:
ordering = ('name',)
def __str__(self):
return self.name
class DisplayedGroup(models.Model):
name = models.CharField(max_length=30, unique=True)
position = models.PositiveSmallIntegerField(default=100)
class Meta:
ordering = ('priority',)
def __str__(self):
return self.name
class Machine(models.Model):
name = models.CharField(max_length=20, unique=True)
settings = models.ManyToManyField(
Setting, through='Arrangement', blank=True
)
class Meta:
ordering = ('name',)
def __str__(self):
return self.name
class Arrangement(models.Model):
machine = models.ForeignKey(Machine, on_delete=models.CASCADE)
setting = models.ForeignKey(Setting, on_delete=models.CASCADE)
displayed_group = models.ForeignKey(
DisplayedGroup, on_delete=models.PROTECT,
default=1)
priority = models.PositiveSmallIntegerField(
default=100,
help_text='Smallest number will be displayed first'
)
class Meta:
ordering = ('priority',)
unique_together = (("machine", "setting"),)
admin.py
class ArrangementInline(admin.TabularInline):
model = Arrangement
extra = 1
class MachineAdmin(admin.ModelAdmin):
inlines = (ArrangementInline,)
If I have 3 settings added on inline form and 1 extra, I have about 10 duplicate queries
SELECT "corps_setting"."id", "corps_setting"."name", "corps_setting"."user_id", "corps_setting"."tagged", "corps_setting"."created", "corps_setting"."modified" FROM "corps_setting" ORDER BY "corps_setting"."name" ASC
- Duplicated 5 times
SELECT "corps_displayedgroup"."id", "corps_displayedgroup"."name", "corps_displayedgroup"."color", "corps_displayedgroup"."priority", "corps_displayedgroup"."created", "corps_displayedgroup"."modified" FROM "corps_displayedgroup" ORDER BY "corps_displayedgroup"."priority" ASC
- Duplicated 5 times.
Could someone please tell me what I'm doing wrong right here? I've spent 3 days trying to figure the problem out myself without luck.
The issue gets worse when I have about 50 settings inlines of a Machine, I will have ~100 queries.
Here is the screenshot
I've assembled a generic solution based on #makaveli's answer that doesn't seem to have problem mentioned in the comments:
class CachingModelChoicesFormSet(forms.BaseInlineFormSet):
"""
Used to avoid duplicate DB queries by caching choices and passing them all the forms.
To be used in conjunction with `CachingModelChoicesForm`.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
sample_form = self._construct_form(0)
self.cached_choices = {}
try:
model_choice_fields = sample_form.model_choice_fields
except AttributeError:
pass
else:
for field_name in model_choice_fields:
if field_name in sample_form.fields and not isinstance(
sample_form.fields[field_name].widget, forms.HiddenInput):
self.cached_choices[field_name] = [c for c in sample_form.fields[field_name].choices]
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
kwargs['cached_choices'] = self.cached_choices
return kwargs
class CachingModelChoicesForm(forms.ModelForm):
"""
Gets cached choices from `CachingModelChoicesFormSet` and uses them in model choice fields in order to reduce
number of DB queries when used in admin inlines.
"""
#property
def model_choice_fields(self):
return [fn for fn, f in self.fields.items()
if isinstance(f, (forms.ModelChoiceField, forms.ModelMultipleChoiceField,))]
def __init__(self, *args, **kwargs):
cached_choices = kwargs.pop('cached_choices', {})
super().__init__(*args, **kwargs)
for field_name, choices in cached_choices.items():
if choices is not None and field_name in self.fields:
self.fields[field_name].choices = choices
All you'll need to do is subclass your model from CachingModelChoicesForm and use CachingModelChoicesFormSet in your inline class:
class ArrangementInlineForm(CachingModelChoicesForm):
class Meta:
model = Arrangement
exclude = ()
class ArrangementInline(admin.TabularInline):
model = Arrangement
extra = 50
form = ArrangementInlineForm
formset = CachingModelChoicesFormSet
EDIT 2020:
Check out the answer by #isobolev below who's taken this answer and improved on it to make it more generic. :)
This is pretty much normal behaviour in Django - it doesn't do the optimization for you, but it gives you decent tools to do it yourself. And don't sweat it, 100 queries isn't really a big problem (I've seen 16k queries on one page) that needs fixing right away. But if your amounts of data are gonna increase rapidly, then it's wise to deal with it of course.
The main weapons you'll be armed with are queryset methods select_related() and prefetch_related(). There's really no point of going too deeply into them since they're very well documented here, but just a general pointer:
use select_related() when the object you're querying has only one related object (FK or one2one)
use prefetch_related() when the object you're querying has multiple related objects (the other end of FK or M2M)
And how to use them in Django admin, you ask? Elementary, my dear Watson. Override the admin page method get_queryset(self, request) so it would look sth like this:
from django.contrib import admin
class SomeRandomAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super().get_queryset(request).select_related('field1', 'field2').prefetch_related('field3')
EDIT: Having read your comment, I realise that my initial interpretation of your question was absolutely wrong. I do have multiple solutions for your problem as well and here goes that:
The simple one that I use most of the time and recommend: just replace the Django default select widgets with raw_id_field widgets and no queries are made. Just set raw_id_fields = ('setting', 'displayed_group') in the inline admin and be done for.
But, if you don't want to get rid of the select boxes, I can give some half-hacky code that does the trick, but is rather lengthy and not very pretty. The idea is to override the formset that creates the forms and specify choices for these fields in the formset so that they're only queried once from the database.
Here it goes:
from django import forms
from django.contrib import admin
from app.models import Arrangement, Machine, Setting, DisplayedGroup
class ChoicesFormSet(forms.BaseInlineFormSet):
setting_choices = list(Setting.objects.values_list('id', 'name'))
displayed_group_choices = list(DisplayedGroup.objects.values_list('id', 'name'))
def _construct_form(self, i, **kwargs):
kwargs['setting_choices'] = self.setting_choices
kwargs['displayed_group_choices'] = self.displayed_group_choices
return super()._construct_form(i, **kwargs)
class ArrangementInlineForm(forms.ModelForm):
class Meta:
model = Arrangement
exclude = ()
def __init__(self, *args, **kwargs):
setting_choices = kwargs.pop('setting_choices', [((), ())])
displayed_group_choices = kwargs.pop('displayed_group_choices', [((), ())])
super().__init__(*args, **kwargs)
# This ensures that you can still save the form without setting all 50 (see extra value) inline values.
# When you save, the field value is checked against the "initial" value
# of a field and you only get a validation error if you've changed any of the initial values.
self.fields['setting'].choices = [('-', '---')] + setting_choices
self.fields['setting'].initial = self.fields['setting'].choices[0][0]
self.fields['setting'].empty_values = (self.fields['setting'].choices[0][0],)
self.fields['displayed_group'].choices = displayed_group_choices
self.fields['displayed_group'].initial = self.fields['displayed_group'].choices[0][0]
class ArrangementInline(admin.TabularInline):
model = Arrangement
extra = 50
form = ArrangementInlineForm
formset = ChoicesFormSet
def get_queryset(self, request):
return super().get_queryset(request).select_related('setting')
class MachineAdmin(admin.ModelAdmin):
inlines = (ArrangementInline,)
admin.site.register(Machine, MachineAdmin)
If you find something that could be improved or have any questions, let me know.
Nowadays, (kudos to that question), BaseFormset receives a form_kwargs attribute.
The ChoicesFormSet code in the accepted answer could be slightly modified as such:
class ChoicesFormSet(forms.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
setting_choices = list(Setting.objects.values_list('id', 'name'))
displayed_group_choices = list(DisplayedGroup.objects.values_list('id', 'name'))
self.form_kwargs['setting_choices'] = self.setting_choices
self.form_kwargs['displayed_group_choices'] = self.displayed_group_choices
The rest of the code stays intact, as desscribed in the accepted answer:
class ArrangementInlineForm(forms.ModelForm):
class Meta:
model = Arrangement
exclude = ()
def __init__(self, *args, **kwargs):
setting_choices = kwargs.pop('setting_choices', [((), ())])
displayed_group_choices = kwargs.pop('displayed_group_choices', [((), ())])
super().__init__(*args, **kwargs)
# This ensures that you can still save the form without setting all 50 (see extra value) inline values.
# When you save, the field value is checked against the "initial" value
# of a field and you only get a validation error if you've changed any of the initial values.
self.fields['setting'].choices = [('-', '---')] + setting_choices
self.fields['setting'].initial = self.fields['setting'].choices[0][0]
self.fields['setting'].empty_values = (self.fields['setting'].choices[0][0],)
self.fields['displayed_group'].choices = displayed_group_choices
self.fields['displayed_group'].initial = self.fields['displayed_group'].choices[0][0]
class ArrangementInline(admin.TabularInline):
model = Arrangement
extra = 50
form = ArrangementInlineForm
formset = ChoicesFormSet
def get_queryset(self, request):
return super().get_queryset(request).select_related('setting')
class MachineAdmin(admin.ModelAdmin):
inlines = (ArrangementInline,)
admin.site.register(Machine, MachineAdmin)
I have a model form, which I'm trying to pass a model instance to initialize values:
class ProjectModelForm(ModelForm):
class meta:
model = Project
def __init__(self, project=None, *args, **kwargs):
super(ProjectModelForm, self).__init__(*args, **kwargs)
if project:
self.fields['zipcode'].initial = project.zipcode
The problem is that the field seems to be populated with a tuple:
(u'90210',)
This happens even when I hardcode with a value I know to be an integer or string:
self.fields['zipcode'].initial = 90210 renders as (90210,).
self.fields['zipcode'].initial = '90210' renders as ('90210',).
Could someone explain what is happening here, and suggest the best route to rendering the result as a simple string?
Any help much appreciated.
EDIT
models.py:
class Project(models.Model):
...
zipcode = models.CharField(max_length=5, null=True, blank=True)
You can pass an initial dictionary of default values when initializing the form:
In View:
initial = {}
if project:
initial.update({'zipcode': project.zipcode})
form = ProjectModelForm(initial=initial)
I have two models related by a foreign key:
# models.py
class TestSource(models.Model):
name = models.CharField(max_length=100)
class TestModel(models.Model):
name = models.CharField(max_length=100)
attribution = models.ForeignKey(TestSource, null=True)
By default, a django ModelForm will present this as a <select> with <option>s; however I would prefer that this function as a free form input, <input type="text"/>, and behind the scenes get or create the necessary TestSource object and then relate it to the TestModel object.
I have tried to define a custom ModelForm and Field to accomplish this:
# forms.py
class TestField(forms.TextInput):
def to_python(self, value):
return TestSource.objects.get_or_create(name=value)
class TestForm(ModelForm):
class Meta:
model=TestModel
widgets = {
'attribution' : TestField(attrs={'maxlength':'100'}),
}
Unfortunately, I am getting: invalid literal for int() with base 10: 'test3' when attempting to check is_valid on the submitted form. Where am I going wrong? Is their and easier way to accomplish this?
Something like this should work:
class TestForm(ModelForm):
attribution = forms.CharField(max_length=100)
def save(self, commit=True):
attribution_name = self.cleaned_data['attribution']
attribution = TestSource.objects.get_or_create(name=attribution_name)[0] # returns (instance, <created?-boolean>)
self.instance.attribution = attribution
return super(TestForm, self).save(commit)
class Meta:
model=TestModel
exclude = ('attribution')
There are a few problems here.
Firstly, you have defined a field, not a widget, so you can't use it in the widgets dictionary. You'll need to override the field declaration at the top level of the form.
Secondly get_or_create returns two values: the object retrieved or created, and a boolean to show whether or not it was created. You really just want to return the first of those values from your to_python method.
I'm not sure if either of those caused your actual error though. You need to post the actual traceback for us to be sure.
TestForm.attribution expects int value - key to TestSource model.
Maybe this version of the model will be more convenient for you:
class TestSource(models.Model):
name = models.CharField(max_length=100, primary_key=True)
Taken from:
How to make a modelform editable foreign key field in a django template?
class CompanyForm(forms.ModelForm):
s_address = forms.CharField(label='Address', max_length=500, required=False)
def __init__(self, *args, **kwargs):
super(CompanyForm, self).__init__(*args, **kwargs)
try:
self.fields['s_address'].initial = self.instance.address.address1
except ObjectDoesNotExist:
self.fields['s_address'].initial = 'looks like no instance was passed in'
def save(self, commit=True):
model = super(CompanyForm, self).save(commit=False)
saddr = self.cleaned_data['s_address']
if saddr:
if model.address:
model.address.address1 = saddr
model.address.save()
else:
model.address = Address.objects.create(address1=saddr)
# or you can try to look for appropriate address in Address table first
# try:
# model.address = Address.objects.get(address1=saddr)
# except Address.DoesNotExist:
# model.address = Address.objects.create(address1=saddr)
if commit:
model.save()
return model
class Meta:
exclude = ('address',) # exclude form own address field
This version sets the initial data of the s_address field as the FK from self, during init , that way, if you pass an instance to the form it will load the FK in your char-field - I added a try and except to avoid an ObjectDoesNotExist error so that it worked with or without data being passed to the form.
Although, I would love to know if there is a simpler built in Django override.