Django - Custom Authentication & correct way to use Managers - python

I've hacked my way to getting my code to work, but I'm pretty sure I'm not doing it as it was intended.
My constraint is I want to have separate DB and UI layers, so I have all the DB-logic encapsulated in SPs/functions that are called from Django's view layer. I tried doing this using the included managers, but kept getting this error:
Manager isn't accessible via %s instances" % cls.__name__)
So, I just removed the manager sub-class and kept going. It works with some extra hacks, but it doesn't feel right. My question is, how do I get my code to work, but still inheriting the stuff from the appropriate managers (i.e. BaseUserManager)? Here's the code:
models.py
from __future__ import unicode_literals
from django.db import models
from UsefulFunctions.dbUtils import *
from django.contrib.auth.models import AbstractBaseUser
class MyUserManager():
# Bypassing BaseUserManager which includes these methods: normalize_email, make_random_password, get_by_natural_key
# Create new user
def create_user(self, password, usertype = None, firstname = None, lastname = None, phonenumber = None, emailaddress = None):
user = MyUser( # TO-DO: Replace MyUser with "get_user_model" reference
userid=None,
usertype=usertype,
firstname=firstname,
lastname=lastname,
phonenumber=phonenumber,
emailaddress=emailaddress
)
# Hash and save password
user.set_password(password)
# Save user data
user.save()
return user
def upsertUser(self, myUser):
return saveDBData('SP_IGLUpsertUser',
(
myUser.userid,
myUser.usertype,
myUser.firstname,
myUser.lastname,
myUser.phonenumber,
myUser.emailaddress,
myUser.password,
myUser.last_login,
None,
)
)
# Create custom base user
class MyUser(AbstractBaseUser):
# Define attributes (inherited class includes password + other fields)
userid = models.IntegerField(unique=True)
usertype = models.CharField(max_length=2)
firstname = models.CharField(max_length=100)
lastname = models.CharField(max_length=100)
phonenumber = models.CharField(max_length=25)
emailaddress = models.CharField(max_length=250)
# Define data manager
MyUserMgr = MyUserManager()
# Create new constructor
def __init__(self, userid = None, usertype = None, firstname = None, lastname = None, phonenumber = None, emailaddress = None):
super(MyUser, self).__init__() # TO-DO: Convert MyUser to get_user_model()
self.userid = userid
self.usertype = usertype
self.firstname = firstname
self.lastname = lastname
self.phonenumber = phonenumber
self.emailaddress = emailaddress
# Define required fields for AbstractBaseUser class
USERNAME_FIELD = 'userid' # specify how Django recognizes the user
EMAIL_FIELD = 'emailaddress'
REQUIRED_FIELDS = ['usertype','firstname','lastname'] # email and password are required by default
# Define class meta info
class Meta:
managed = False
db_table = 'userprofile'
# Required methods
def get_full_name(self):
return self.firstname + " " + self.lastname + " (" + self.userid + ")"
def get_short_name(self):
return self.userid
def save(self):
return self.MyUserMgr.upsertUser(self)
# Define model managers (interface between DB and objects)
class ItemDataManager():
def getAllItems(self):
return getDBData('SP_IGLGetItem', (None,)) # Use tuple instead of array for input parameters
def getItem(self, myItem):
return getDBData('SP_IGLGetItem', (myItem.itemid,))
def getItemDetail(self, myItem):
return getDBData('SP_IGLGetItemDetail', (myItem.itemid,))
def upsertItem(self, myItem):
return saveDBData('SP_IGLUpsertItem',
(
myItem.itemid,
myItem.itemname,
myItem.itemdescription,
myItem.itemcontactuserid,
)
)
def deleteItem(self, myItem):
return deleteDBData('SP_IGLDeleteItem', (myItem.itemid, None))
# Define data models (i.e. tables)
class Item(models.Model):
# Model properties
itemid = models.IntegerField
itemname = models.CharField(max_length=100)
itemdescription = models.CharField(max_length=5000)
itemcontactuserid = models.IntegerField
# Create Item Data Manager instance
myItemMgr = ItemDataManager()
# Create new constructor
def __init__(self, itemid = None):
super(Item, self).__init__()
self.itemid = itemid
# Define static methods (don't depend on object instance)
#staticmethod
def get_all():
return ItemDataManager().getAllItems()
# Define instance methods
def get(self):
return self.myItemMgr.getItem(self)
# Define instance methods
def get_detail(self):
return self.myItemMgr.getItemDetail(self)
def save(self):
return self.myItemMgr.upsertItem(self)
def delete(self):
return self.myItemMgr.deleteItem(self)
Sample call:
from django.contrib.auth import get_user_model;
get_user_model().MyUserMgr.create_user('mypass','AD','Joe','Smith','1233','joe#smith.com')
This is the line that's giving me trouble:
def save(self):
return self.MyUserMgr.upsertUser(self)
Right now, it works fine. But when I subclass BaseUserManager, I can't get it to work. What am I doing wrong? How should I restructure the code/references to properly use the included manager classes?
I've read all the relevant posts. I'm guessing the answer is in there somewhere, but it's all a jumbled mess to me at this point.
I am using:
Django 1.11
Python 2.7
Postgres 9.6

The error is caused by you trying to access the model manager from the instance.
In save() you're dealing with an instance of the model to be saved, so you can't access the manager. self is an instance (object) of the class, not the class itself.
First of all, I'd swap to the standard django approach with your manager which would be objects = MyUserMgr() so then you can do MyUserModel.objects.all(), MyUserModel.objects.upsertUser() etc.
Normally in Django you'd use a model manager to run queries that you want to use a lot so that you don't have to duplicate them in your views/forms etc.
Then you can just stick to saving the instance in the model save() method to start to try to simplify what you're doing because you've got quite complex already.
Have a look at the docs for Managers; https://docs.djangoproject.com/en/1.11/topics/db/managers/#managers
Then have a look at this really simple approach to extending the user model https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html

Related

Is it possible to override filter lookup with predefined values with custom Manager/Queryset in Django

I am trying to implement the behaviour for my models so that when you are deleting the object(s) it is not deleting physically, but just add some attribute to state that it was deleted.
So I created custom queryset, manager and mixin to apply for each created model:
class StateQuerySet(models.query.QuerySet):
def delete(self):
self.update(active=False)
class StateManager(models.Manager):
def get_queryset(self):
return StateQuerySet(self.model, using=self._db).filter(active=True)
class ModelMixin(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
active = models.BooleanField(default=True)
objects = StateManager()
def delete(self, *args, **kwargs):
self.active = False
self.save()
class Meta:
abstract = True
And the models:
class Organizer(ModelMixin, models.Model):
name = models.CharField(max_length=256)
class EventData(ModelMixin, models.Model):
caption = models.CharField(max_length=512)
description = models.TextField(blank=True, default='')
organizer = models.ForeignKey(Organizer, on_delete=models.CASCADE)
So, the idea, is when I do any:
Organizer.objects.all() / Organizer.objects.filter(name__startswith='<some_start_prefix>')
I will recieve Organizer objects only which are active=True (i.e. "not deleted").
All looks good, but when I have related objects, there is an issue.
If, for example, I do:
EventData.objects.filter(organizer__name__startswith='<some_start_prefix>')
It will return all EventData objects even the 'deleted'.
But if I do:
EventData.objects.filter(organizer__name__startswith='<some_start_prefix>', organizer__active=True)
All works as expected and only 'active' records returned. So I don't want to use:
organizer__active=True
for each query in the views.
I've read the docs but still don't understand how to create this with custom Manager and QuerySet. Could you please help, or guide how to do this? What I am missing?
So to summarize: when I do:
EventData.objects.filter(organizer__name__startswith='<some_start_prefix>')
I want to recieve all EventData objects where Organizer.active set to True, but without writing additional organizer__active=True each time in the client code. Is it possible to do it in the Manager or QuerySet?
Ok, so you can indeed add some custom queries to your model's queryset.
You might do something like;
def filter_active_organizer(self, prefix):
return self.filter(organizer__name__startswith=prefix, organizer__active=True)
Let me illustrate with something from a project I've got;
from django.db.models import Q, QuerySet
from django.utils import timezone
class EventQuerySet(QuerySet):
"""
Custom queryset/manager for the Event model...adds common filtering
operations
"""
def filter_open(self):
"""
Filter events in this queryset to only include those whose entry
process is currently open.
"""
now = timezone.now()
return self.filter(
Q(entry_open__isnull=True) | Q(entry_open__lte=now),
Q(entry_close__isnull=True) | Q(entry_close__gt=now)
)
def filter_closed(self):
"""
Filter events in this queryset to only include those whose entry
process is currently closed.
"""
now = timezone.now()
return self.filter(
(Q(entry_open__isnull=False) & Q(entry_open__gt=now)) |
(Q(entry_close__isnull=False) & Q(entry_close__lte=now))
)
EventManager = EventQuerySet.as_manager()
class Event(models.Model):
class Meta:
app_label = 'entry'
verbose_name = _('Event')
verbose_name_plural = _('Events')
objects = EventManager
These queries can then be accessed using Event.objects.filter_open() or added to admin with a filter class;
class FilterStatus(admin.SimpleListFilter):
"""
List filter to filter events by whether they are open or closed
(this is a computed value)
"""
title = 'entry status'
parameter_name = 'entry_status'
def lookups(self, request, model_admin):
"""
Return the lookup choices for this filter
"""
return [
('open', _('Open')),
('closed', _('Closed')),
]
def queryset(self, request, queryset):
"""
Perform the filtering (if required)
"""
if self.value() == 'open':
return queryset.filter_open()
if self.value() == 'closed':
return queryset.filter_closed()
return queryset

How to properly associate FactoryBoy subfactory with child object

I have some tests that were working when I ran them with regular database objects but are broken now that I am using FactoryBoy factories. I think I understand why they are broken but am struggling with the correct way to set this up.
Here are my factories:
#register
class UserFactory(BaseFactory):
"""User factory."""
username = Sequence(lambda n: 'user{0}'.format(n))
email = Sequence(lambda n: 'user{0}#example.com'.format(n))
password = PostGenerationMethodCall('set_password', 'example')
active = True
class Meta:
"""Factory configuration."""
model = User
#register
class ExperimentFactory(BaseFactory):
"""Experiment Factory."""
date = fake.date_this_decade(before_today=True, after_today=False)
scanner = Iterator(['GE', 'Sie', 'Phi'])
class Meta:
"""Factory configuration."""
model = Experiment
user = factory.SubFactory(UserFactory)
According to this answer and other examples, FactoryBoy is supposed to be handling the foreign key assignment behind the scenes.
But when I try to initialize my ExperimentFactory object in my fixture, I have a problem.
#pytest.fixture(scope='function')
#pytest.mark.usefixtures('db')
def mocked_scan_service(db, mocker, request):
user = UserFactory(password='myprecious')
db.session.add(user)
num_exp, num_scans, exp_id, scan_id, exp_uri, scan_uri = request.param
for i in range(num_exp):
experiment = ExperimentFactory(user_id = user.id)
db.session.add(experiment)
db.session.commit()
ss = ScanService(user.id, experiment.id)
for i in range(num_scans):
ss._add_scan_to_database()
ss.xc.upload_scan = mocker.MagicMock()
ss.xc.upload_scan.return_value = ('/data/archive/subjects/000001', exp_uri, scan_uri)
mocker.spy(ss, '_generate_xnat_identifiers')
ss.param = request.param
return ss
If I don't pass ExperimentFactory a user id, I get this error:
TypeError: __init__() missing 1 required positional argument: 'user_id'
Here's the model; it makes sense to me that the factory needs an argument user_id to initialize:
class Experiment(SurrogatePK, Model):
"""A user's experiment, during which they are scanned."""
__tablename__ = 'experiment'
date = Column(db.Date(), nullable=False)
scanner = Column(db.String(80), nullable=True)
num_scans = Column(db.Integer(), nullable=True, default=0)
xnat_experiment_id = Column(db.String(80), nullable=True)
xnat_uri = Column(db.String(80), nullable=True)
user_id = reference_col('user', nullable=False)
scans = relationship('Scan', backref='experiment')
def __init__(self, date, scanner, user_id, **kwargs):
"""Create instance."""
db.Model.__init__(self, date=date, scanner=scanner, user_id=user_id, **kwargs)
def __repr__(self):
"""Represent instance as a unique string."""
return '<Experiment({date})>'.format(date=self.date)
But if, as written, I explicitly create a user and then pass the user id, it looks like the ExperimentFactory eventually overwrites the foreign key with the SubFactory it generated. So later when I initialize an object called ScanService which must be initialized with a user_id and and experiment_id, my tests fail for one of two reasons. Either I initialize it with the user_id of my explicitly created user, and my tests fail because they don't find any sibling experiments to the experiment that experiment_id belongs to, or I initialize it with experiment.user.id, and my tests fail because they expect one user in the database, and in fact there are two. That latter problem would fairly easy to work around by rewriting my tests, but that seems janky and unclear. How am I supposed to initialize the ExperimentFactory when the Experiment model requires a user_id for initialization?
If anyone has a better solution feel free to comment, but here's what I realized: it really doesn't matter what I pass in for user_id; I just have to pass in something so that model initialization doesn't fail. And passing in user=user at the same time creates the situation I want: all experiments belong to the same user. Now all my tests pass. Here's the modified fixture code; everything else remained the same:
#pytest.fixture(scope='function')
#pytest.mark.usefixtures('db')
def mocked_scan_service(db, mocker, request):
num_exp, num_scans, exp_id, scan_id, exp_uri, scan_uri = request.param
user = UserFactory(password='myprecious')
for i in range(num_exp):
experiment = ExperimentFactory(user_id=user.id, user=user)
db.session.add(experiment)
db.session.commit()
ss = ScanService(experiment.user.id, experiment.id)
for i in range(num_scans):
ss._add_scan_to_database()
ss.xc.upload_scan = mocker.MagicMock()
ss.xc.upload_scan.return_value = ('/data/archive/subjects/000001', exp_uri, scan_uri)
mocker.spy(ss, '_generate_xnat_identifiers')
ss.param = request.param
return ss

Python convert string to class

I want to do a query on the django User table like this:
u = User.objects.filter(member__in = member_list)
where:
class Member(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
dob = models.DateField('Date of Birth', blank=True, null=True)
and member_list is a list of eligible members.
The query works fine but the problem is I do not actually know the model member is called member. It could be called anything.
I store the name of the model I want in a model called Category. I have a link to the name of the model through content_type.Category is defined as:
class Category(models.Model):
name = models.CharField('Category', max_length=30)
content_type = models.ForeignKey(ContentType)
filter_condition = JSONField(default="{}", help_text=_(u"Django ORM compatible lookup kwargs which are used to get the list of objects."))
user_link = models.CharField(_(u"Link to User table"), max_length=64, help_text=_(u"Name of the model field which links to the User table. 'No-link' means this is the User table."), default="No-link")
def clean (self):
if self.user_link == "No-link":
if self.content_type.app_label == "auth" and self.content_type.model == "user":
pass
else:
raise ValidationError(
_("Must specify the field that links to the user table.")
)
else:
if not hasattr(apps.get_model(self.content_type.app_label, self.content_type.model), self.user_link):
raise ValidationError(
_("Must specify the field that links to the user table.")
)
def __unicode__(self):
return self.name
def _get_user_filter (self):
return str(self.content_type.app_label)+'.'+str(self.content_type.model)+'.'+str(self.user_link)+'__in'
def _get_filter(self):
# simplejson likes to put unicode objects as dictionary keys
# but keyword arguments must be str type
fc = {}
for k,v in self.filter_condition.iteritems():
fc.update({str(k): v})
return fc
def object_list(self):
return self.content_type.model_class()._default_manager.filter(**self._get_filter())
def object_count(self):
return self.object_list().count()
class Meta:
verbose_name = _("Category")
verbose_name_plural = _("Categories")
ordering = ('name',)
So I can retrieve the name of the model that links to User but I then need to convert it into a class which I can include in a query.
I can create an object x = category.content_type.model_class() which gives me <class 'cltc.models.Member'> but when I them perform a query s = User.objects.filter(x = c.category.object_list()) I get the error Cannot resolve keyword 'x' into field.
Any thoughts most welcome.
The left hand side of the filter argument is a keyword, not a python object, so x is treated as 'x', and Django expects a field called x.
To get around this, you can ensure that x is a string, and then use the python **kwarg syntax:
s = User.objects.filter(**{x: c.category.object_list()})
Thanks to https://stackoverflow.com/a/4720109/823020 for this.

Display relevant records from many to many in django-tables2

OK, so I have an Item class that has a many-to-many attribute to User through a 'Roles' class. I am trying to create a django-table for the Items such that out of any of the roles attached to the item, if the current User is attached to that role, the name of the role displays. I hope that makes some sort of sense. Here's what I have so far, which I didn't really expect to work because I don't see how the Table class can know about the request/user. I'm stuck.
models.py
class Item(models.Model):
name = models.CharField(max_length=255)
owner = models.ForeignKey(User, related_name='Owner')
roles = models.ManyToManyField(User, through='Role')
class Role(models.Model):
role_type = models.ForeignKey(RoleType)
user = models.ForeignKey(User)
item = models.ForeignKey(Item)
tables.py
class OwnedTable(tables.Table):
roles = tables.Column()
user = request.user
def render_roles(self):
for role in roles:
if role.User == user:
return role.role_type
else:
pass
class Meta:
model = Item
attrs = {"class": "paleblue"}
fields = ('id', 'name', 'owner', 'roles')
You can get the request object from self.context. So if you only need request.user, that's one way to do it.
class OwnedTable(tables.Table):
roles = tables.Column(empty_values=())
def render_roles(self):
user = self.context["request"].user
...
Otherwise, #mariodev's solution works.
It seems like there's no way of using auth user without some overriding.
You can override __init__ for our table class like this:
class OwnedTable(tables.Table):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(OwnedTable, self).__init__(*args, **kwargs)
then, inside view, you call table with user argument, like so
table = OwnedTable(Person.objects.all(), user=request.user)
now you can use self.user inside render_roles method to refer to the currently logged in user.
Another solution is shown on https://github.com/bradleyayers/django-tables2/issues/156, which worked for me after some adjustments for my setup.
Given that a Person would have an M2M to Contacts, and you want to display all contacts for that Person in django-tables2, then the following would do:
class PersonTable(tables.Table):
person_contacts = tables.Column(accessor="contacts", verbose_name="Contacts")
def render_person_contacts(self, value, table):
clist = ""
cfirst = True
conts = list(value.all())
for c in conts:
if not cfirst:
clist += "<br />"
else:
cfirst = False
print c.id
uri = reverse('cont_detail', kwargs={'pk': c.id})
clist += '' + c.name + '' + ' (' + c.relation + ')'
return mark_safe(clist)
You basically add a column with a non-existent name, set the accessor to the M2M field name, then call render_<newly_added_column> where you obtain what you need from value.

How do I filter values in a Django form using ModelForm?

I am trying to use the ModelForm to add my data. It is working well, except that the ForeignKey dropdown list is showing all values and I only want it to display the values that a pertinent for the logged in user.
Here is my model for ExcludedDate, the record I want to add:
class ExcludedDate(models.Model):
date = models.DateTimeField()
reason = models.CharField(max_length=50)
user = models.ForeignKey(User)
category = models.ForeignKey(Category)
recurring = models.ForeignKey(RecurringExclusion)
def __unicode__(self):
return self.reason
Here is the model for the category, which is the table containing the relationship that I'd like to limit by user:
class Category(models.Model):
name = models.CharField(max_length=50)
user = models.ForeignKey(User, unique=False)
def __unicode__(self):
return self.name
And finally, the form code:
class ExcludedDateForm(ModelForm):
class Meta:
model = models.ExcludedDate
exclude = ('user', 'recurring',)
How do I get the form to display only the subset of categories where category.user equals the logged in user?
You can customize your form in init
class ExcludedDateForm(ModelForm):
class Meta:
model = models.ExcludedDate
exclude = ('user', 'recurring',)
def __init__(self, user=None, **kwargs):
super(ExcludedDateForm, self).__init__(**kwargs)
if user:
self.fields['category'].queryset = models.Category.objects.filter(user=user)
And in views, when constructing your form, besides the standard form params, you'll specify also the current user:
form = ExcludedDateForm(user=request.user)
Here example:
models.py
class someData(models.Model):
name = models.CharField(max_length=100,verbose_name="some value")
class testKey(models.Model):
name = models.CharField(max_length=100,verbose_name="some value")
tst = models.ForeignKey(someData)
class testForm(forms.ModelForm):
class Meta:
model = testKey
views.py
...
....
....
mform = testForm()
mform.fields["tst"] = models.forms.ModelMultipleChoiceField(queryset=someData.objects.filter(name__icontains="1"))
...
...
Or u can try something like this:
class testForm(forms.ModelForm):
class Meta:
model = testKey
def __init__(self,*args,**kwargs):
super (testForm,self ).__init__(*args,**kwargs)
self.fields['tst'].queryset = someData.objects.filter(name__icontains="1")
I know this is old; but its one of the first Google search results so I thought I would add how I found to do it.
class CustomModelFilter(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "%s %s" % (obj.column1, obj.column2)
class CustomForm(ModelForm):
model_to_filter = CustomModelFilter(queryset=CustomModel.objects.filter(active=1))
class Meta:
model = CustomModel
fields = ['model_to_filter', 'field1', 'field2']
Where 'model_to_filter' is a ForiegnKey of the "CustomModel" model
Why I like this method:
in the "CustomModelFilter" you can also change the default way that the Model object is displayed in the ChoiceField that is created, as I've done above.
is the best answer:
BookDemoForm.base_fields['location'] = forms.ModelChoiceField(widget=forms.Select(attrs={'class': 'form-control select2'}),queryset=Location.objects.filter(location_for__fuel=True))

Categories