I have two models that will use the same CardNumberField() to store credit card numbers. How can I add a custom method to the field to mask the card numbers?
I have created the CardNumberField() which inherits from models.Charfield:
# CARD NUMBER FIELD
class CardNumberField(models.CharField):
description = _('card number')
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 19
super().__init__(*args, **kwargs)
The CardNumberField() is then imported and used in my customers/models.py:
# CARD MODEL
class Card(models.Model):
number = CardNumberField()
...
def __str__(self):
return 'Card [{number}]'.format(number=self.number)
...and in my transactions/models.py:
# TRANSACTION MODEL
class Transaction(models.Model):
card_number = CardNumberField()
...
def __str__(self):
return 'Transaction ...'
So, how can I add the following method to my CardNumberField() to be used by both of my models?
def masked_number(self):
# display masked card number
number = self.number
return number[-4:].rjust(len(number), '#')
Also, how will I grab this field method in a DRF serializer class?
You can override the contribute_to_class method to not only contribute the field, but also include an extra method:
from functools import partialmethod
def _mask_number(self, field):
number = getattr(self, field.attname)
return number[-4:].rjust(len(number), '#')
# CARD NUMBER FIELD
class CardNumberField(models.CharField):
description = _('card number')
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 19
super().__init__(*args, **kwargs)
def contribute_to_class(self, cls, name, **kwargs):
super().contribute_to_class(cls, name, **kwargs)
setattr(
cls, f'masked_{self.name}',
partialmethod(_mask_number, field=self)
)
If you add a field foo to a model class, it will automatically add a masked_foo method to that class. This thus also means that if you have two or more CardNumberFields, it will add two or more masked_foo methods.
Use an abstract model instead:
class ModelWithCardNumber(models.Model):
card_number = models.CharField(max_length=19)
#property
def masked_number(self):
return self.card_number[-4:].rjust(len(number), '#')
class Meta:
abstract = True
class Card(ModelWithCardNumber):
def __str__(self):
return 'Card [{number}]'.format(number=self.number)
class Transaction(ModelWithCardNumber):
def __str__(self):
return 'Transaction ...'
Now in your serializer you can access Card.masked_number and Transaction.masked_number.
Related
I am building a FAQ app.
Model flow Topic -> Section -> Article.
Article has a FK to Section which has a FK to Topic.
In my create article from I want to take in the Topic_Pk so when the user selects a Section the choice selection is limited to just the Sections attached under the Topic.
I am using get_from_kwarg to pass the Topic_Pk from the url to __init__ in the form. I keep getting a TypeError __init__() got an unexpected keyword argument 'topic_pk'. I do not want to pop the data or set topic_pk=None in the __init__ parameters as this would invalidate the whole point.
What is it I am missing to allow me to use this variable?
Url:
url(r'^ironfaq/(?P<topic_pk>\d+)/article/create$', ArticleCreateView.as_view()),
View:
class ArticleCreateView(CreateView):
model = Article
form_class = CreateArticleForm
template_name = "faq/form_create.html"
success_url = "/ironfaq"
def get_form_kwargs(self):
kwargs = super(ArticleCreateView,self).get_form_kwargs()
kwargs.update(self.kwargs)
return kwargs
Form:
class CreateArticleForm(forms.ModelForm):
section = forms.ModelChoiceField(queryset=Section.objects.none())
def __init__(self, *args, **kwargs):
super(CreateArticleForm, self).__init__(*args, **kwargs)
self.fields['section'].queryset = Section.objects.filter(topic_pk=self.kwargs['topic_pk'])
class Meta:
model = Article
widgets = {
'answer': forms.Textarea(attrs={'data-provide': 'markdown', 'data-iconlibrary': 'fa'}),
}
fields = ('title','section','answer')
Model:
class Article(Audit):
title = models.CharField(max_length=255)
sort = models.SmallIntegerField()
slug = models.SlugField()
section = models.ForeignKey(Section,on_delete=models.CASCADE)
answer = models.TextField()
vote_up = models.IntegerField()
vote_down = models.IntegerField()
view_count = models.IntegerField(default=0)
class Meta:
verbose_name_plural = "articles"
def __str__(self):
return self.title
def total_votes(self):
return self.vote_up + self.vote_down
def percent_yes(self):
return (float(self.vote_up) / self.total_votes()) * 100
def get_absolute_url(self):
return ('faq-article-detail',(), {'topic__slug': self.section.topic.slug,
'section__slug': self.section.slug, 'slug': self.slug})
For your current __init__ signature, you must pop topic_pk from kwargs before you call super(), otherwise you'll get the TypeError.
In your question, you say that popping the value would 'invalidate the whole point', but I think you're mistaken. You can still use the topic_pk value after calling super().
class CreateArticleForm(forms.ModelForm):
section = forms.ModelChoiceField(queryset=Section.objects.none())
def __init__(self, *args, **kwargs):
topic_pk = kwargs.pop('topic_pk')
super(CreateArticleForm, self).__init__(*args, **kwargs)
self.fields['section'].queryset = Section.objects.filter(topic_pk=topic_pk)
Another approach would be to use topic_pk as a named argument. Note that this changes the signature of the __init__ method, so it might break other code (for example if you had CreateArticleForm(request.POST) somewhere else).
def __init__(self, topic_pk=None, *args, **kwargs):
super(CreateArticleForm, self).__init__(*args, **kwargs)
self.fields['section'].queryset = Section.objects.filter(topic_pk=topic_pk)
I have a models like these
class Campaign(models.Model):
campaign_name= models.CharField(max_length=30)
user=models.ForeignKey(User)
rows=models.IntegerField(default=3)
columns=models.IntegerField(default=1)
def __str__(self): # __unicode__ on Python 2
return self.campaign_name+": "+self.campaign_desc
def save(self, *args, **kwargs):
print("Doing Custom Task ")
super(Campaign, self).save(*args, **kwargs) # Call the "real" save() method.
class Item(models.Model):
campaign=models.ForeignKey(Campaign)
item_name=models.CharField(max_length=70)
item_ID=models.CharField(max_length=400)
def __str__(self):
return self.item_name
I have registered Campaign in admin using admin.site.register(Campaign,CampaignAdmin) and want the number of items in each campaign to be rows X columns of campaign (Foreign key).
Q1) If I validate it using the save override method in the place of print("Doing Custom Task ") , I couldn't save the instance when it's created.
Q2) It would be also nice to have the number of items to be filled to show up appropriately. Now I have
class ItemInline(admin.TabularInline):
model = Item
#extra = 4
in admin.py Basically I want the extra parameter to be rows X columns
If you want to do some custom validation, you'd better do it via forms, or (as a last resort), using the Model.clean* methods family.
class Campaign(models.Model):
def clean(self):
if self.pk:
if self.item_set.count() > 5 # Or whatever number you need
raise ValidationError(_('Too much items for me.'))
Override the get_extra() method for your inline:
class ItemInline(admin.TabularInline):
extra = 4
def get_extra(self, request, obj=None, **kwargs):
if obj:
return obj.rows * obj.columns
return self.extra
My model.py:
class RelayAddress(models.Model):
id = models.AutoField(primary_key=True,default=0)
sister_relay_relation = models.ManyToManyField('self', through='RelaySisterRelation',symmetrical=False)
def save(self, *args, **kwargs):
self.update_time = int(time.time())
super(RelayAddress,self).save(*args, **kwargs)
class RelaySisterRelation(models.Model):
id = models.AutoField(primary_key=True,default=0)
relay = models.ForeignKey(RelayAddress,related_name="relay")
sister_relay = models.ForeignKey(RelayAddress,related_name="sister_relay")
My admin.py
class RelaySisterRelationForm(forms.ModelForm):
relay=forms.ModelMultipleChoiceField(label=u'relay',widget=forms.CheckboxSelectMultiple(),queryset=RelayAddress.objects.all())
sister_relay=forms.ModelMultipleChoiceField(label=u'sister_relay',widget=forms.CheckboxSelectMultiple(),queryset=RelayAddress.objects.all())
def save(self, *args, **kwargs):
return super(RelaySisterRelationForm, self).save(*args,**kwargs)
And my view.py is null, then I get a ValueError:
Cannot assign "[<RelayAddress: RelayAddress object>]": "RelaySisterRelation.relay" must be a "RelayAddress" instance.
And how to solve this problem.
RelaySisterRelation.relay is a ForeignKey to RelayAddress meaning it can only store a references to one RelayAddress but your RelaySisterRelationForm.relay uses ModelMultipleChoiceField which is for many-to-many relations so returns a (potentially empty) list of RelayAddress instances.
I have following form:
class EmailPreferences(forms.ModelForm):
""" Base form used for fields that are always required """
def __init__(self, *args, **kw):
super(EmailPreferences, self).__init__(*args, **kw)
class Meta:
model = teacher_email_preferences
exclude = ['id', 'c_user']
def save(self, force_insert=False, force_update=False, commit=True):
obj = super(EmailPreferences, self).save(commit=commit)
return obj
As you can see model name is fixed which is teacher_email_preferences. But my site has two types of user one is Teacher other is Student. I do not want to create a separate form. So i want to change model to student_email_preferences when initiating this form. How can I pass a model here when doing form = EmailPreferences()?
You can't. But you can use a closure instead.
def emailform(emailmodel):
class EmailPreferences(forms.ModelForm):
...
class Meta:
model = emailmodel
...
return EmailPreferences
...
form = emailform(teacher_email_preferences)()
In the app we're developing using Django, in some cases we need to automatically assign permissions to users for some models, that has owners (there is no rule for field's name, it can be "user", "owner", "coach" etc., also there can by more than one field.) My solution is to create a decorator containing those fields names, that will be put before model definition, like this (not using django-specific code in samples):
#auto_assign_perms('owner', 'user')
class Test(Base):
pass
Let's assume that Base is an abstract class deriving after Django's Model class, where I add functionality to assign permissions after object is saved. For now I only print a list of users assigned to the class. Below you can find code for the decorator and Base class:
class auto_assign_perms(object):
def __init__(self, *users):
self.users = users
def __call__(self, cls):
cls.owners.update(self.users)
return cls
class Base(object):
owners = set()
def save(self, *args, **kwargs):
for owner in self.owners:
print owner,
print
And my models could look like this:
#auto_assign_perms('owner', 'user')
class Test(Base):
pass
#auto_assign_perms('coach')
class Test2(Base):
pass
The problem is that both child classes contains all three fields ('owner', 'user', 'coach'), altough print self.__class__.__name__ in Base.save() method properly shows "Test" or "Test2". I tried to add classmethod get_owners() in Base class and then iterating over its results, but it doesn't helps.
How can I solve this? Maybe I should use metaclasses (I don't get them yet)? Thanks in advance.
You need to set the list of owners, not update:
class auto_assign_perms(object):
def __init__(self, *users):
self.users = users
def __call__(self, cls):
cls.owners = set(self.users) # <- here
return cls
#some tests
#auto_assign_perms('owner', 'user')
class Test(Base):
pass
#auto_assign_perms('coach')
class Test2(Base):
pass
t = Test()
t.save()
t = Test2()
t.save()
>>>
owner user
coach
You are using owners as a class variable of Base so whenever you change owners the change will be seen in all the derived classes.
To fix that you should define the owners variable as class variable of the derived classes:
class Base(object):
def save(self, *args, **kwargs):
for owner in self.owners:
print owner,
print
#auto_assign_perms('owner', 'user')
class Test(Base):
owners = set()
#auto_assign_perms('coach')
class Test2(Base):
owners = set()
Call me paranoia but i find this solution more elegant and that because i don't think you need owners to be a class variable at all:
def auto_assign_perms(*users):
def class_wrapper(cls):
class ClassWrapper(cls):
def __init__(self, owners=users):
super(cls, self).__init__(owners=owners)
ClassWrapper.__name__ = cls.__name__
ClassWrapper.__module__ = cls.__module__
return ClassWrapper
return class_wrapper
class Base(object):
def __init__(self, owners=None):
if owners is None:
owners = set()
self.owners = owners
def save(self, *args, **kwargs):
for owner in self.owners:
print owner,
print
#auto_assign_perms('owner', 'user')
class Test1(Base):
pass
#auto_assign_perms('coach')
class Test2(Base):
pass
class Test3(Base):
pass
t = Test1(); t.save() # owner user
t = Test2(); t.save() # coach
t = Test3(); t.save() #