django - logging edit event - need to get id of logged in user - django

I've a product-stock model as given below.
TRANSACTION_TYPE=(('I','Stock In'),('O','Stock Out'))
class Stock(models.Model):
product=models.ForeignKey('product.Product', blank=False,null=False)
date=models.DateField(blank=False, null=False,)
quantity=models.PositiveIntegerField(blank=False, null=False)
ttype=models.CharField(max_length=1,verbose_name="Transaction type",choices=TRANSACTION_TYPE, blank=False, db_index=True)
I need to log the update activity on this model, along with the id of the user who updated it.
ACTIONS=(('EC','Edit Category'),
('EG','Edit Group'),
('EP','Edit Product'),
('ES','Edit Stock'))
class MyLog(models.Model):
user=models.ForeignKey(auth.models.User, blank=False)
action= models.CharField(max_length=2, choices=ACTIONS, null=False,blank=False)
date=models.DateTimeField(blank=False, auto_now=True)
data = JSONField()
I've the added following code to the Stock model.
def __init__(self, *args, **kwargs):
super(Stock, self).__init__(*args, **kwargs)
if self.pk != None :
self.__important_fields = ['product','date', 'quantity', 'ttype', ]
for field in self.__important_fields:
setattr(self, '__original_%s' % field, getattr(self, field))
field_name='__original_%s' % field
def save(self, *args, **kwargs):
if self.pk != None :
print("Editing")
flag=False
log=MyLog(user=?,action='ES')
log.data=[]
for field in self.__important_fields:
original=getattr(self, '__original_%s' % field)
if original != getattr(self, field):
flag=True
log.data.append({field : str(original)})
if flag:
log.save()
else:
print("Adding")
super(Stock, self).save(*args, **kwargs)
This works, when I hard code a user object into the line log=MyLog(user=?,action='ES').
I need to log the id of the user who performed this edit operation.
How can I achieve this?
Thanks.

Here's how I finally achieved my goal.
Instead of logging the event from the model, I switched my code to the forms.
Here's my final code.
mylog app model
ACTIONS=(('EC','Edit Category'),
('EG','Edit Group'),
('EP','Edit Product'),
('ES','Edit Stock'))
class MyLog(models.Model):
user=models.ForeignKey(settings.AUTH_USER_MODEL, blank=False)
model_id=models.IntegerField(default=0)
action= models.CharField(max_length=2, choices=ACTIONS, null=False,blank=False)
date=models.DateTimeField(blank=False, auto_now=True)
old_data = JSONField(default=None)
new_data = JSONField(default=None)
stock app - update view
class UpdateStock(UpdateView):
model=Stock
form_class=UpdateStockForm
def get_form_kwargs(self):
kwargs = super( UpdateStock, self).get_form_kwargs()
kwargs.update({'user_id': self.request.user.id})
return kwargs
stock app - update form
class UpdateStockForm(forms.ModelForm):
def __init__(self,pk= None, *args, **kwargs):
self.user_id=kwargs.pop('user_id')
super(UpdateStockForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(UpdateStockForm, self).clean()
quantity = cleaned_data.get('quantity')
date= cleaned_data.get('date')
priv_quantity=self.instance.quantity
priv_date=self.instance.date
if priv_quantity!=quantity or priv_date != date:
#log!
log=MyLog(user=auth.models.User.objects.get(pk=self.user_id),action='ES', model_id=self.instance.id)
log.old_data=[]
log.old_data.append({'date' : str(priv_date), 'quantity':priv_quantity })
log.new_data=[]
log.new_data.append({ 'date' : str(date), 'quantity':quantity })
log.save()
return cleaned_data

Related

Django models.py (API result) - retrieve current post to add api result

I'm new to Django I got an issue. I don't know how to retrieve the current post inside of models.py. I've tried different way for that.
'QuerySet' object has no attribute 'aliments'
or no error and no add to Post from ListAliments
get_object_or_404(Post, id=kwargs['id'])
here is my models.py
class ListAliments(models.Model):
name = models.CharField(max_length=40, unique=True)
slug = models.SlugField(editable=False)
status = models.IntegerField(choices=STATUS, default=1)
def save(self, *args,**kwargs):
if not self.slug:
self.slug = unique_slugify(self, slugify(self.name))
super(ListAliments, self).save(*args, **kwargs)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=190)
url_image = models.URLField(max_length=200, default=None)
aliments = models.ManyToManyField('ListAliments',blank=True, related_name='listaliments_post')
...
def save(self, *args, **kwargs):
if not self.slug:
self.slug = unique_slugify(self, slugify(self.title))
super(Post, self).save(*args, **kwargs) -> First save for Post which has not no ID
...
if self.url_image:
request = ...
response = ...
if response:
names = []
for concept in response.outputs[0].data.concepts:
current_aliments = ListAliments.objects.filter(name=concept.name)
current_post = Post.objects.filter(url_image=self.url_image) #get_object_or_404(Post, id=kwargs['id'])
if current_aliments.count()<1:
create_aliments = self.aliments.create(name=concept.name)
current_post.aliments.add(create_aliments)
else:
existed_aliments = ListAliments.objects.get(name=concept.name)
current_post.aliments.add(existed_aliments)
super().save(*args, **kwargs)
Post.objects.filter(url_image=self.url_image) returns QuerySet
in order to get object call first so Post.objects.filter(url_image=self.url_image).first(); note that you can get None

Making field required = False on ModelForm

I've got a model of UserCoupon that a user can create and edit. Upon edit, I only want them to be able to edit the 'code' field on the instance of UserCoupon if there are no orders associated with that code. When there are orders associated with that coupon code, rather than outputting {{form.code}} on the edit coupon form, I'm outputting {{form.instance.code}}. When the user attempts to submit the form, I get an error saying that the field is required.
How can I make this field not required or otherwise address this situation so that the user can submit the form when one of the fields defined for the model form shows up in the template as an instance of the field rather than an input box?
Models.py
class UserCoupon(models.Model):
code = models.CharField(max_length=15, unique=True)
amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
forms.py
class EditUserCouponForm (forms.ModelForm):
class Meta:
model = UserCoupon
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user',None)
super(EditUserCouponForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(EditUserCouponForm, self).clean()
template
{% if coupon_order_count > 0 %}
{{form.instance.code}}
{% else %}
{{form.code}}
{% endif %}
Thanks!
Simply add blank=True to the model field:
class UserCoupon(models.Model):
code = models.CharField(max_length=15, unique=True, blank=True)
See this question for a good explanation on the difference between blank and null.
Probably you can do it like this:
class EditUserCouponForm(forms.ModelForm):
class Meta:
model = UserCoupon
fields = ['code']
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user',None)
super(EditUserCouponForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.order_set.all().exists(): #checking if there are any orders with this coupon
self.fields['code'].widget.attrs['disabled'] = 'disabled'
And in views, make sure when editing pass the instance like this:
def coupon_view(request, pk):
coupon = UserCoupon.objects.get(pk=pk)
form = EditUserCouponForm(request.POST or None, user=request.user, instance=coupon)
if request.method == "GET":
return render('template', context={'form':form})
Probably you don't need to use coupon_order_count in template
I used this in my form and it worked:
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user',None)
self.coupon_order_count = kwargs.pop('coupon_order_count',None)
super(EditUserCouponForm, self).__init__(*args, **kwargs)
if int(self.coupon_order_count) > 0:
self.fields['code'].required = False

Related model not updating in save override of a model being saved with ModelFormset

So I have Transaction model that is FK-d to a Share. In an 'Account' view, I have a ModelFormset of these Transactions and I can save multiple transactions by looping through the forms and saving them.
On my Transaction's save() method I try and update the balance on the linked Share. this works if I save one transaction, but when I POST my ModelFormset with multiple transactions, everytime I hit the self.share.balance = self.share.balance + amt line in the Transaction save() override (that is for every new Transaction), the share.balance is what it was before any of the previous transactions in the formset were saved.
Does anyone know why the added amount to share balance from a previous saved transaction is not carried on the subsequent saves (why only the last Transaction's amount will be added to share balance)?
Transaction model which should update balance on parent-model Share
class Transaction(models.Model):
share = models.ForeignKey(Share, on_delete=models.CASCADE, null=True, blank=True)
account = models.ForeignKey(Account, on_delete=models.CASCADE, null=True, blank=True)
db_cr = models.CharField(choices=DBCR, max_length=2)
amt = models.DecimalField('Amount', max_digits=11, decimal_places=2)
post_dt = models.DateTimeField('Post Time', null=True, blank=True)
def save(self, *args, **kwargs):
if not self.pk:
...
if self.share:
if self in self.share.transaction_set.all():
logging.error('Transaction %s already posted' % self.id)
return False
amt = self.amt if self.db_cr == 'cr' else -self.amt
self.share.balance = self.share.balance + amt
self.share.save()
Share Model
class Share(models.Model):
name = models.CharField(max_length=80)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
definition = models.ForeignKey(ShareDef, on_delete=models.PROTECT)
balance = models.DecimalField('Balance', max_digits=11, decimal_places=2, default=0)
def __str__(self):
return '%s %s %s %s'%(self.account,
self.name,
self.definition.sym_code,
self.balance )
def save(self, *args, **kwargs):
if not self.pk:
if not self.name:
self.name = self.definition.name
super(Share, self).save(*args, **kwargs)
In view, I have a Transaction formset
#...in view
TranFormSet = modelformset_factory(Transaction, exclude=('origin','ach_entry'), extra=1)
if request.method=='POST':
...
tran_formset = TranFormSet(request.POST)
...
if tran_formset.is_valid():
for form in tran_formset:
tran = form.save(commit=False)
tran.account = account
tran.origin = 'tt'
tran.save()
else:
#...following kind of weird because of how I'm setting querysets of ModelChoiceFields
kwargs = {'account_instance': account}
tran_formset = TranFormSet(queryset=Transaction.objects.none())
tran_formset.form = (curry(TranForm, **kwargs))
Form
class TranForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
account_instance = kwargs.pop('account_instance', None)
super(TranForm, self).__init__(*args, **kwargs)
if account_instance:
self.fields['share'].queryset = account_instance.share_set.all()
if self.instance.pk:
del self.fields['share']
class Meta:
model=Transaction
exclude=['origin', 'ach_entry', 'account']
post_dt = forms.DateTimeField(initial=datetime.date.today(), widget=forms.TextInput(attrs=
{
'class': 'datepicker'
}))
share = forms.ModelChoiceField(empty_label='---------', required=False, queryset=Share.objects.all())
It's unclear what may be causing the issue, but it may be helpful to perform the update of the self.share.balance in a single update() query. This can be done using F expressions:
from django.db.models import F
class Transaction(models.Model):
# ...
def update_share_balance(self):
if self.db_cr == "cr":
amount = self.amt
else:
amount = -self.amt
# By using the queryset update() method, we can perform the
# change in a single query, without using a potentially old
# value from `self.share.balance`
return Share.objects.filter(id=self.share_id).update(
balance=F("balance") + amount
)
def save(self, *args, **kwargs):
if not self.pk:
# ...
if self.share:
# ...
self.update_share_balance()
# Also, be sure to call the super().save() method at the end!
super().save(*args, **kwargs)

Two fields related in Django

I need to update my table every time a new value of "sku" is entered (not to create a new entry), but it does have to happen only if the "client" selected is the same. If the "client" is different, then the model should add a new object with the same "sku", but with different "clients".
I have tried to do the following in my models.py:
class ProductList(models.Model):
id_new = models.IntegerField(primary_key=True)
sku = models.CharField(primary_key=False, max_length=200)
client = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
name = models.CharField(max_length=256)
description = models.CharField(max_length=1000)
storage = models.CharField(max_length=256)
cost_price = models.CharField(max_length=256)
sell_price = models.CharField(max_length=256)
ncm = models.CharField(max_length=256)
inventory = models.IntegerField(null=True)
class Meta:
unique_together = (('sku', 'client'),)
But it is not working. How can I make that work?
You can try like this:
# form
class MyForm(forms.ModelForm):
class Meta:
model = ProductList
def save(self, *args, **kwargs:
client = self.cleaned_data.get('client') # get client from form cleaned_data
if hasattr(self.instance, 'pk') and self.instance.client != client: # check if client match's already existing instance's client
self.instance.pk = None # make a duplicate instance
self.instance.client = client # change the client
return super(MyForm, self).save(*args, **kwargs)
# views.py
# ...
def my_view(request, id):
instance = get_object_or_404(ProductList, id=id)
form = MyForm(request.POST or None, instance=instance)
if form.is_valid():
form.save()
return redirect('next_view')
return render(request, 'my_template.html', {'form': form})
Update
Um you can override the model as well. you can try like this:
# Untested Code but should work
def save(self, *args, **kwargs):
if self.pk:
current_instance = self.__class__.objects.get(pk=self.pk)
if current_instance.client != self.client:
self.pk = None
return super(ProductList, self).save(*args, **kwargs)

ModelForm and Model validation playing together

I have the following Model :
class Advertisement(models.Model):
slug = models.UUIDField(default=uuid4, blank=True, editable=False)
advertiser = models.ForeignKey(Advertiser)
position = models.SmallIntegerField(choices=POSITION_CHOICES)
share_type = models.CharField(max_length=80)
country = CountryField(countries=MyCountries, default='DE')
postal_code = models.CharField(max_length=8, null=True, blank=True)
date_from = models.DateField()
date_to = models.DateField()
Based on Advertiser, position, type country and postal code this stores adverisements with range date_from and date_to.
advertiser, position, share_type, country and postal_code
are coming from the request and are fetched in
class CreateAdvertisment(LoginRequiredMixin, CreateView):
# Some usefull stuff
def dispatch(self, request, *args, **kwargs):
self.advertiser = Advertiser.objects.get(user=self.request.user)
self.share_type = self.kwargs.get('share_type', None)
self.country = self.kwargs.get('country', None)
self.postal_code = self.kwargs.get('postal_code', None)
self.position = int(self.kwargs.get('position', None))
self.position_verbose = verbose_position(self.position)
ret = super(CreateAdvertisment, self).dispatch(request, *args, **kwargs)
return ret
Without any validation for checking date_from, date_to. I can simply do
def form_valid(self, form):
form.instance.advertiser = self.advertiser
form.instance.share_type = self.share_type
form.instance.country = self.country
form.instance.postal_code = self.postal_code
form.instance.position = self.position
ret = super(CreateAdvertisment, self).form_valid(form)
return ret
and I am done. Unfortunatly I cannot do this as I do have to check for valid time Frames for the Advertisment to avoid Double Bookings of the same time. I do this in the model with the following :
def clean(self):
ret = super(Advertisement, self).clean()
print ("country [%s] position [%s] share_type [%s] postal_code [%s]" % (self.country,
self.position, self.share_type, self.postal_code))
if self.between_conflict():
raise ValidationError("Blocks between timeframe")
elif self.end_conflict():
raise ValidationError("End occupied")
elif self.during_conflict():
raise ValidationError("Time Frame complete occupied")
elif self.start_conflict():
raise ValidationError("Start Occupied")
return ret
def start_conflict(self):
start_conflict = Advertisement.objects.filter(country=self.country,
position=self.position,
share_type=self.share_type,
postal_code=self.postal_code).filter(
date_from__range=(self.date_from, self.date_to))
return start_conflict
This works well and I filter out any Conflict for the given period. Problem is that I do not have the instance variables as they are set in view.form_valid() and model.clean() is called by the form validation process.
I do have an chicken egg problem here. I am thinking about setting the requests parameters to the form kwargs in
def get_form_kwargs(self, **kwargs):
kwargs = super(CreateAdvertisment, self).get_form_kwargs()
kwargs['advertiser'] = self.advertiser
kwargs['position'] = self.position
....
and then putting them into the form instance in form.init()
def __init__(self, *args, **kwargs):
advertiser = kwargs.pop('advertiser')
position = kwargs.pop('position')
# .. and so on
super(AdvertismentCreateForm, self).__init__(*args, **kwargs)
For some reasons I do not think this is very pythonic. Does anybody have a better idea? I will post my solution.
I think that overriding get_form_kwargs is ok. If all the kwargs are instance attributes, then I would update the instance in the get_form_kwargs method. Then you shouldn't have to override the form's __init__, or update the instance's attributes in the form_valid method.
def get_form_kwargs(self, **kwargs):
kwargs = super(CreateAdvertisment, self).get_form_kwargs()
if kwargs['instance'] is None:
kwargs['instance'] = Advertisement()
kwargs['instance'].advertiser = self.advertiser
...
return kwargs
In the model's clean method, you can now access self.advertiser.
alasdairs proposal works fine I have the following now :
def get_form_kwargs(self, **kwargs):
kwargs = super(CreateAdvertisment, self).get_form_kwargs()
if kwargs['instance'] is None:
kwargs['instance'] = Advertisement()
kwargs['instance'].advertiser = self.advertiser
kwargs['instance'].share_type = self.share_type
kwargs['instance'].country = self.country
kwargs['instance'].postal_code = self.postal_code
kwargs['instance'].position = self.position
return kwargs
def form_valid(self, form):
ret = super(CreateAdvertisment, self).form_valid(form)
return ret
Of course there is no need to override form_valid anymore. I have just included here in order to display that we do not set the instance fields anymore as this is done in get_form_kwargs() already