Django - multiple profiles - django

In my project I have two different types of users: teacher and student, each with their own profile data.
After searching for the best approach it seems the way to go forward is using multi-table inheritance:
class BaseProfile(models.Model):
user = models.OneToOneField(User)
profile = models.CharField (max_length=10, choices={'teacher', 'student'})
# other common fields
class Teacher(BaseProfile):
# teacher specific fields
class Student(BaseProfile):
# student specific fields
And in settings.py: AUTH_PROFILE_MODULE = myapp.BaseProfile.
Now I want to implement the same functionalities as in django-profiles:
create profiles
edit profiles
display profiles
I have a good idea how to do the edit and display part when I have the correct value in the field profile of BaseProfile.
The problem:
Now I want the creation of the profile to be done automatically (and in the right db: Teacher or Student) directly when a user is created by using a signal.
The field profile should contain the value "student" when the user registers through the site via the registration form. The value should be "teacher" when the admin creates a new user through the admin interface.
Anyone an idea how I can accomplish this? Probably I need to write a custom signal, something like the below, and send it from the User Model, but didn't found a working solution yet:
def create_user_profile(sender, instance, request, **kwargs):
if request.user.is_staff:
BaseProfile(user=instance, profile='teacher').save()
else:
BaseProfile(user=instance, profile='student').save()
Other and better approaches are of course also welcome!
Thanks!

In my opinion it isn't a good approach.
I would recommend doing 1 unified profile which will contain an option:
user_type = models.CharField(choices=[your_choices], max_length=4)
Then in models you would create two forms - 1 for teacher and 1 for student.
class ProfileFOrm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(BaseProfileForm, self).__init__(*args, **kwargs)
for name in self.fields:
self.fields[name].required = True
class TeacherProfile(ProfileForm):
class Meta:
model = Profile
fields = ('your_fields')
class StudentProfile(ProfileForm):
class Meta:
model = Profile
fields = ('school')
That's just my idea for that :)
Edited
Profile edition:
view.
def profile(request):
p = get_objects_or_404(ProfileModel, user=request.user)
return TemplateResponse(request, 'template.html', {'profile': p})
In models we need a function to check if user is a student or a teacher, so:
class Profile(models.Model):
... your fields here...
def get_student(self):
return self.user_type == 1
In templates:
{% if profile.get_student%}
>>>>get all data for students ex: <<<<
{{profile.name}}
{% endif %}
{% if profile.get_teacher %}
....

Related

Wagtail - how to preopulate fields in admin form?

I would like to pre-populate fields in wagtail page admin. Particularly I would like to take username of currently logged admin/editor user and fill it in the form as a string. A simplified version of my page looks like this:
class ItemPage(Page):
author = models.CharField(max_length=255, default="")
content_panels = Page.content_panels + [
FieldPanel('author'),
]
I do not want to set a default value in the author field in the model - it should be user specific.
I do not want to use the save method or signal after the model is saved/altered. The user should see what is there and should have the ability to change it. Also, the pages will be generated automatically without the admin interface.
I think that I need something like https://stackoverflow.com/a/14322706/3960850 but not in Django, but with the Wagtail ModelAdmin.
How to achieve this in Wagtail?
Here is an example based on gasmans comment and the documentation that accompanies the new code they linked:
from wagtail.admin.views.pages import CreatePageView, register_create_page_view
from myapp.models import ItemPage
class ItemPageCreateView(CreatePageView):
def get_page_instance(self):
page = super().get_page_instance()
page.author = 'Some default value'
return page
register_create_page_view(ItemPage, ItemPageCreateView.as_view())
You could also do this by overriding the models init method, but the above is much nicer
class ItemPage(Page):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
author = kwargs['owner'].username
except (AttributeError, KeyError):
pass
else:
self.author = author

Change fields of ModelForm dynamically

Is it possible to change what fields are displayed in a ModelForm, dynamically?
I am trying to show only a small number of fields in a ModelForm when the user adds a new instance (of the Model) from the frontend (using an add form) but larger number of fields when the user edits an instance (using an edit form).
The Form class looks something like this:
class SchoolForm(ModelForm):
class Meta:
model = School
#want to change the fields below dynamically depending on whether its an edit form or add form on the frontend
fields = ['name', 'area', 'capacity', 'num_of_teachers']
widgets = {
'area': CheckboxSelectMultiple
}
labels = {
'name': "Name of the School",
'num_of_teachers': "Total number of teachers",
}
Trying to avoid having two separate classes for add and edit since that doesnt seem DRYish. I found some SO posts with the same question for the admin page where we could override get_form() function but that does not apply here.
Also, this answer suggests using different classes as the normal way and using dynamic forms as an alternative. Perhaps dynamics forms is the way forward here but not entirely sure (I also have overridden __init__() and save() methods on the SchoolForm class).
I'm not suere if is a correct way, but i use some method in class to add fields or delete-it. I used like this:
class someForm(forms.ModelForm):
class Meta:
model = Foo
exclude = {"fieldn0","fieldn1"}
def __init__(self, *args, **kwargs):
super(someForm, self).__init__(*args, **kwargs)
self.fields['foofield1'].widget.attrs.update({'class': 'form-control'})
if self.instance.yourMethod() == "FooReturn":
self.fields['city'].widget.attrs.update({'class': 'form-control'})
else:
if 'city' in self.fields: del self.fields['city']
Hope it helps.

Add method doesnt work when trying to establish m2m relationships using post_save in Django

My Content model has a many-to-many relationship to the Tag model. When I save a Content object, I want to add the relationships dynamically. I'm doing this the following way.
def tag_content(obj):
for tag in Tag.objects.all():
print tag
obj.tags.add(tag)
obj.is_tagged = True
obj.save()
class Tag(models.Model):
name = models.CharField(max_length=255)
class Content(models.Model):
title = models.CharField(max_length=255)
is_tagged = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag, blank=True)
def save(self, *args, **kwargs):
super(Content, self).save(*args, **kwargs)
#receiver(post_save, sender = Content)
def update_m2m_relationships_on_save(sender, **kwargs):
if not kwargs['instance'].is_tagged:
tag_content(kwargs['instance'])
The tag_content function runs, however, the m2m relationships are not established. Im using Django 1.9.8 btw. This makes no sense. What am I missing? Moreover, if I do something like tag_content(content_instance) in shell, then the tags are set, so the function is ok. I guess the problem is in the receiver. Any help?
Edit
My question has nothing to do with m2m_changed, as I have said, creating a Content object in shell works perfectly. Therefore, the problem lies in the admin panel's setup.
Ok so I solved the problem. Basically, this has something to do with how Django handles its form in the admin panel. When trying to add the Contents from admin, I kept the tags field empty, thinking the tag_content function would handle it. However, that is exactly where the problem was, as creating a Content from shell tagged it just fine. In other words, changing the admin panel to something like this solved my problem :
from django.contrib import admin
from myapp.models import *
from django import forms
class ContentCreationForm(forms.ModelForm):
class Meta:
model = Content
fields = ('title',)
class ContentChangeForm(forms.ModelForm):
class Meta:
model = Content
fields = ('title', 'is_tagged', 'tags')
class ContentAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj is None:
return ContentCreationForm
else:
return ContentChangeForm
admin.site.register(Tag)
admin.site.register(Content, ContentAdmin)
When trying to create a new Content, only the 'title' field is presented. This solves the problem.

Django "Enter a list of values" form error when rendering a ManyToManyField as a Textarea

I'm trying to learn Django and I've ran into some confusing points. I'm currently having trouble creating a movie using a form. The idea of the form is to give the user any field he'd like to fill out. Any field that the user fills out will be updated in its respective sql table (empty fields will be ignored). But, the form keeps giving me the error "Enter a list of values" when I submit the form. To address this, I thought stuffing the data from the form into a list and then returning that list would solve this.
The first idea was to override the clean() in my ModelForm. However, because the form fails the is_valid() check in my views, the cleaned_data variable in clean() doesn't contain anything. Next, I tried to override the to_python(). However, to_python() doesn't seem to be called.
If I put __metaclass__ = models.SubfieldBase in the respective model, I receive the runtime error
"TypeError: Error when calling the
metaclass bases
metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the
metaclasses of all its bases"
My approach doesn't seem to work. I'm not sure how to get around the 'Enter a list of values" error! Any advice?
Here is the relevant code (updated):
models.py
""" Idea:
A movie consists of many equipments, actors, and lighting techniques. It also has a rank for the particular movie, as well as a title.
A Theater consists of many movies.
A nation consists of many theaters.
"""
from django.db import models
from django.contrib.auth.models import User
class EquipmentModel(models.Model):
equip = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class ActorModel(models.Model):
actor = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class LightModel(models.Model):
light = models.CharField(max_length=20)
# user = models.ForeignKey(User)
class MovieModel(models.Model):
# __metaclass__ = models.SubfieldBase
rank = models.DecimalField(max_digits=5000, decimal_places=3)
title = models.CharField(max_length=20)
equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
actors = models.ManyToManyField(ActorModel, blank=True, null=True)
lights = models.ManyToManyField(LightModel, blank=True, null=True)
class TheaterModel(models.Model):
movies = models.ForeignKey(MovieModel)
class NationModel(models.Model):
theaters = models.ForeignKey(TheaterModel)
=====================================
forms.py
"""
These Modelforms tie in the models from models.py
Users will be able to write to any of the fields in MovieModel when creating a movie.
Users may leave any field blank (empty fields should be ignored, ie: no updates to database).
"""
from django import forms
from models import MovieModel
from django.forms.widgets import Textarea
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
self.fields["equipments"].widget = Textarea()
self.fields["lights"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
print 'cleaning actors'
return [data]
class Meta:
model = MovieModel
=============================================
views.py
""" This will display the form used to create a MovieModel """
from django.shortcuts import render_to_response
from django.template import RequestContext
from forms import MovieModelForm
def add_movie(request):
if request.method == "POST":
form = MovieModelForm(request.POST)
if form.is_valid():
new_moviemodel = form.save()
return HttpResponseRedirect('/data/')
else:
form = MovieModelForm()
return render_to_response('add_movie_form.html', {form:form,}, context_instance=RequestContext(request))
The probable problem is that the list of values provided in the text area can not be normalized into a list of Models.
See the ModelMultipleChoiceField documentation.
The field is expecting a list of valid IDs, but is probably receiving a list of text values, which django has no way of converting to the actual model instances. The to_python will be failing within the form field, not within the form itself. Therefore, the values never even reach the form.
Is there something wrong with using the built in ModelMultipleChoiceField? It will provide the easiest approach, but will require your users to scan a list of available actors (I'm using the actors field as the example here).
Before I show an example of how I'd attempt to do what you want, I must ask; how do you want to handle actors that have been entered that don't yet exist in your database? You can either create them if they exist, or you can fail. You need to make a decision on this.
# only showing the actor example, you can use something like this for other fields too
class MovieModelForm(forms.ModelForm):
actors_list = fields.CharField(required=False, widget=forms.Textarea())
class Meta:
model = MovieModel
exclude = ('actors',)
def clean_actors_list(self):
data = self.cleaned_data
actors_list = data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(','):
try:
actor = Actor.objects.get(actor=actor_name)
except Actor.DoesNotExist:
if FAIL_ON_NOT_EXIST: # decide if you want this behaviour or to create it
raise forms.ValidationError('Actor %s does not exist' % actor_name)
else: # create it if it doesnt exist
Actor(actor=actor_name).save()
return actors_list
def save(self, commit=True):
mminstance = super(MovieModelForm, self).save(commit=commit)
actors_list = self.cleaned_data.get('actors_list', None)
if actors_list is not None:
for actor_name in actors_list.split(","):
actor = Actor.objects.get(actor=actor_name)
mminstance.actors.add(actor)
mminstance.save()
return mminstance
The above is all untested code, but something approaching this should work if you really want to use a Textarea for a ModelMultipleChoiceField. If you do go down this route, and you discover errors in my code above, please either edit my answer, or provide a comment so I can. Good luck.
Edit:
The other option is to create a field that understands a comma separated list of values, but behaves in a similar way to ModelMultipleChoiceField. Looking at the source code for ModelMultipleChoiceField, it inhertis from ModelChoiceField, which DOES allow you to define which value on the model is used to normalize.
## removed code because it's no longer relevant. See Last Edit ##
Edit:
Wow, I really should have checked the django trac to see if this was already fixed. It is. See the following ticket for information. Essentially, they've done the same thing I have. They've made ModelMutipleChoiceField respect the to_field_name argument. This is only applicable for django 1.3!
The problem is, the regular ModelMultipleChoiceField will see the comma separated string, and fail because it isn't a List or Tuple. So, our job becomes a little more difficult, because we have to change the string to a list or tuple, before the regular clean method can run.
class ModelCommaSeparatedChoiceField(ModelMultipleChoiceField):
widget = Textarea
def clean(self, value):
if value is not None:
value = [item.strip() for item in value.split(",")] # remove padding
return super(ModelCommaSeparatedChoiceField, self).clean(value)
So, now your form should look like this:
class MovieModelForm(forms.ModelForm):
actors = ModelCommaSeparatedChoiceField(
required=False,
queryset=Actor.objects.filter(),
to_field_name='actor')
equipments = ModelCommaSeparatedChoiceField(
required=False,
queryset=Equipment.objects.filter(),
to_field_name='equip')
lights = ModelCommaSeparatedChoiceField(
required=False,
queryset=Light.objects.filter(),
to_field_name='light')
class Meta:
model = MovieModel
to_python AFAIK is a method for fields, not forms.
clean() occurs after individual field cleaning, so your ModelMultipleChoiceFields clean() methods are raising validation errors and thus cleaned_data does not contain anything.
You haven't provided examples for what kind of data is being input, but the answer lies in form field cleaning.
http://docs.djangoproject.com/en/dev/ref/forms/validation/#cleaning-a-specific-field-attribute
You need to write validation specific to that field that either returns the correct data in the format your field is expecting, or raises a ValidationError so your view can re-render the form with error messages.
update: You're probably missing the ModelForm __init__ -- see if that fixes it.
class MovieModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieModelForm, self).__init__(*args, **kwargs)
self.fields["actors"].widget = Textarea()
def clean_actors(self):
data = self.cleaned_data.get('actors')
# validate incoming data. Convert the raw incoming string
# to a list of ids this field is expecting.
# if invalid, raise forms.ValidationError("Error MSG")
return data.split(',') # just an example if data was '1,3,4'

Django registration and multiple profiles

I'm using django-registration in my application. I want to create different kinds of users with different profiles.
For example, one user is a teacher and another user is a student.
How can I modify registration to set the user_type and create the right profile?
Long answer :p
I've found The Missing Manual post invaluable for this kind of problem as it explains many of features of the django-profiles and django-registration systems.
I'd suggest using multi table inheritance on the single profile you're allowed to set via the AUTH_PROFILE_MODULE
For instance
#models.py
class Profile(models.Model):
#add any common fields here (first_name, last_name and email come from User)
#perhaps add is_student or is_teacher properites here
#property
def is_student(self):
try:
self.student
return True
except Student.DoesNotExist:
return False
class Teacher(Profile):
#teacher fields
class Student(Profile):
#student fields
django-registration uses signals to notify you of a registration. You should be creating the profile at that point so you are confident that calls to user.get_profile() will always return a profile.
The signal code used is
#registration.signals.py
user_registered = Signal(providing_args=["user", "request"])
Which means when handling that signal you have access to the request made. So when you POST the registration form include a field that identifies what type of user to create.
#signals.py (in your project)
user_registered.connect(create_profile)
def create_profile(sender, instance, request, **kwargs):
from myapp.models import Profile, Teacher, Student
try:
user_type = request.POST['usertype'].lower()
if user_type == "teacher": #user .lower for case insensitive comparison
Teacher(user = instance).save()
elif user_type == "student":
Student(user = instance).save()
else:
Profile(user = instance).save() #Default create - might want to raise error instead
except KeyError:
Profile(user = instance).save() #Default create just a profile
If you want to add anything to the model that is created, that isn't covered by default field values, at registration time you can obviously pull that from the request.
http://docs.djangoproject.com/en/1.2/topics/auth/#groups
Django groups are a great way to define what you are looking for.
You can have one User extended profile that will contain all attributes of teachers and students.
class MasterProfile(models.Model):
user = models.ForeignKey(User, unique=True)
# add all the fields here
Then define groups: teacher and student and you associate each MasterProfile to either a teacher or a student.
Django Group table can help you define your various roles and allocate users to groups correctly.
I had the same issue and I tried the answer suggested by Chris, however it didn't work for me.
I'm only a newbie in Django, but I think the args taken by handler create_profile should match those under providing_args by signal user_registered, and in Chris's answer they don't (I think they probably match those passed by signal post_save, which I've seen used in the Missing Manual that he quotes)
I modified his code to make args match:
def create_profile(sender, **kwargs):
"""When user is registered also create a matching profile."""
request, instance = kwargs['request'], kwargs['user']
# parse request form to see whether profile is student or teacher
try:
user_type = request.POST['usertype'].lower()
print(user_type)
if user_type == "teacher": #user .lower for case insensitive comparison
Teacher(user = instance).save()
elif user_type == "student":
Student(user = instance).save()
else:
Userprofile(user = instance).save() #Default create - might want to raise error instead
except KeyError:
Userprofile(user = instance).save() #Default create just a profile
and seems to be working now