I am using allauth and want a user to be able to invite people via email to sign up to an account. When the new user signs up, I want to set their 'school' field to be the same as the user who invited them. In other words, they are part of the same school and I want to store that info when saving the new user.
To achieve this, I have an invite button which sends an email with the original user's school passed as a token, like so:
class AddUsers(TemplateView):
template_name = 'schools/add-users.html'
def get(self, request, *args, **kwargs):
add_user_form = AddUserForm()
context = {
'add_user_form': add_user_form
}
return render(request, self.template_name, context)
def post(self, request, *args, **kwargs):
if 'add_users' in request.POST:
add_user_form = AddUserForm(request.POST)
if add_user_form.is_valid():
to_email_address = add_user_form.cleaned_data.get('email_field')
user = request.user
school = request.user.school
mail_subject = 'Invitation to create an account'
url = request.build_absolute_uri(reverse('account_signup'))
uid = urlsafe_base64_encode(force_bytes(school.pk))
token = account_activation_token.make_token(school)
activation_link = "{0}?uid={1}&token{2}".format(url, uid, token)
message = 'Hi,\n\nYour colleague ' + user.first_name + ' has invited you to sign up.\n\n'
message += 'Click the activation link below\n\n'
message += activation_link
email = EmailMessage(mail_subject, message, to=[to_email_address])
email.send()
return HttpResponseRedirect(reverse('schools:add-users',))
return HttpResponseRedirect(reverse('settings', ))
I override the allauth Signup form like this for a regular new user, but when the user has been invited by another user (i.e. via the email activation link with the school token), I plan to hide the school field and save the object held in the token value instead:
class SignupForm(ModelForm):
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=150)
class Meta:
model = School
fields = ['school_name', 'authority']
def __init__(self, *args, **kwargs):
super(SignupForm, self).__init__(*args, **kwargs)
self.fields['first_name'].widget.attrs['class'] = 'form-control'
self.fields['first_name'].widget.attrs['placeholder'] = 'First name'
self.fields['last_name'].widget.attrs['class'] = 'form-control'
self.fields['last_name'].widget.attrs['placeholder'] = 'Last name'
self.fields['school_name'].widget.attrs['class'] = 'form-control'
self.fields['school_name'].widget.attrs['placeholder'] = 'School name'
self.fields['authority'].queryset = Authority.objects.get_all_authorities()
self.fields['authority'].label = 'Local authority'
self.fields['authority'].widget.attrs['class'] = 'form-control'
self.fields['authority'].empty_label = 'No local authority'
def signup(self, request, user):
school_name = self.cleaned_data['school_name']
first_name = self.cleaned_data['first_name']
last_name = self.cleaned_data['last_name']
authority = self.cleaned_data['authority']
school = School.objects.new_school_account(school_name, authority, 28)
user.school = school
user.first_name = first_name
user.last_name = last_name
user.save()
This works and sends an email with the token which correctly redirects to the allauth account_signup page. I can see how I can use the code in this solution to convert the token back again, but I don't know how/where I can actually access the token using allauth Signup in order to save the school when saving the new user details.
So my question is - I am passing a token to the allauth account_signup page but how can I get the token so I can process it?
token = request.GET.get('token')
You can get it in de dispatch method in the SignupView like this:
class MySignupView(SignupView):
def dispatch(self, request, *args, **kwargs):
token = request.GET.get('token')
return super(MySignupView, self).dispatch(request, *args, **kwargs)
Related
I have a foreign key on my models like Patient, and Doctor, which point to a Clinic class. So, the Patient and Doctor are supposed to belong to this Clinic alone. Other Clinics should not be able to see any detail of these Models.
The models look like this:
class Clinic(models.Model):
clinicid = models.AutoField(primary_key=True, unique=True)
name = models.CharField(max_length=60, unique=True)
label = models.SlugField(max_length=25, unique=True)
email = models.EmailField(max_length=100, default='')
mobile = models.CharField(max_length=15, default='')
...
class Doctor(models.Model):
# Need autoincrement, unique and primary
docid = models.AutoField(primary_key=True, unique=True)
name = models.CharField(max_length=200)
username = models.CharField(max_length=15)
regid = models.CharField(max_length=15, default="", blank=True)
...
linkedclinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
class Patient(models.Model):
cstid = models.AutoField(primary_key=True, unique=True)
date_of_registration = models.DateField(default=timezone.now)
name = models.CharField(max_length=35, blank=False)
ageyrs = models.IntegerField(blank=True)
agemnths = models.IntegerField(blank=True)
dob = models.DateField(null=True, blank=True)
...
linkedclinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
class UserGroupMap(models.Model):
id = models.AutoField(primary_key=True, unique=True)
user = models.ForeignKey(
User, related_name='target_user', on_delete=models.CASCADE)
group = models.ForeignKey(UserGroup, on_delete=models.CASCADE)
clinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
...
From my Vue app, I will post using Axios to the django app which uses DRF, and thus get serialized data of Patients and Doctors. It all works fine if I try to use the following sample code in function view:
#api_view(['GET', 'POST'])
def register_patient_vue(request):
if request.method == 'POST':
print("POST details", request.data)
data = request.data['registration_data']
serializer = customerSpecialSerializer(data=data)
if serializer.is_valid():
a = serializer.save()
print(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
print("Serializer is notNot valid.")
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Sample output:
POST details {'registration_data': {'name': 'wczz', 'ageyrs': 21, 'agemonths': '', 'dob': '', 'gender': 'unspecified', 'mobile': '2', 'email': '', 'alternate': '', 'address': '', 'marital': 'unspecified', 'city': '', 'occupation': '', 'linkedclinic': 10}}
data: {'name': 'wczz', 'ageyrs': 21, 'agemonths': '', 'dob': '', 'gender': 'unspecified', 'mobile': '2', 'email': '', 'alternate': '', 'address': '', 'marital': 'unspecified', 'city': '', 'occupation': '', 'linkedclinic': 10}
However, I need to authenticate the request by special custom authentication. I have another class called UserGroupMap which has Foreign Keys for both User and Clinic, so that if there is a match for a filter for the clinic and user, in the map, it will authenticate. Else it should fail authentication and the data should not be retrieved or serializer saved.
In my previous simple pure django project I used to employ a custom permission function, and decorating my view with it:
#handle_perm(has_permission_level, required_permission='EDIT_CLINICAL_RECORD', login_url='/clinic/')
def some_function(request, dept_id):
....
Some code which runs after authentication
And it would use the following:
def handle_perm(test_func, required_permission=None, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
"""
Decorator for views that checks that the user passes the given test,
redirecting to the log-in page if necessary. The test should be a callable
that takes the user object and returns True if the user passes.
"""
def decorator(view_func):
#wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
print(f"Required permission level is {required_permission}")
if has_permission_level(request, required_permission):
print("User has required permission level..Allowing entry.")
return view_func(request, *args, **kwargs)
print("FAILED! User does not have required permission level. Access blocked.")
path = request.build_absolute_uri()
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(
path, resolved_login_url, redirect_field_name)
return _wrapped_view
return decorator
def has_permission_level(request, required_permission, clinic=None):
print("has_permission_level was called.")
user = request.user
print(f'user is {user}')
clinic=clinic_from_request(request)
print(f"has_permission_level called with clinic:{clinic}")
if clinic is None:
print("clinic is none")
return HttpResponseRedirect('/accounts/login/')
group_maps = UserGroupMap.objects.filter(user=user, clinic=clinic)
print(f"No: of UserGroupMap memberships: {len(group_maps)}")
if len(group_maps) < 1:
# There are no UserGroupMap setup for the user. Kindly set them up.\nHint:Admin>Manage users and groups>Users
return False
# Now checking Group memberships whether the user has any with permisison
for map in group_maps:
rolesmapped = GroupRoleMap.objects.filter(group=map.group)
if len(rolesmapped) < 1:
print(f"No permission roles.")
else:
for rolemap in rolesmapped:
print(f"{rolemap.role}", end=",")
if rolemap.role.name == required_permission:
print(
f"\nAvailable role of [{map.group}] matched required permission of [{required_permission}] in {clinic.name} [Ok]")
return True
return False
I need to build a custom authentication using DRF, so that it reads the POSTed data, and checks the linkedclinic value, and employs similiar logic.
I started like this:
def has_permission_POST(request, required_permission, clinic=None):
print("has_permission_POST was called.")
user = request.user
print(f'user is {user}')
if request.method == 'POST':
print(request)
print(dir(request))
print("POST details: POST:", request.POST, "\n")
print("POST details: data:", request.data, "\n")
....
# Further logic to check the mapping
return True
else:
print("Not a valid POST")
return Response("INVALID POST", status=status.HTTP_400_BAD_REQUEST)
# And decorating my DRF view:
#handle_perm(has_permission_POST, required_permission='EDIT_CLINICAL_RECORD', login_url='/clinic/')
#api_view(['GET', 'POST'])
def register_patient_vue(request):
if request.method == 'POST':
print("POST details", request.data)
data = request.data['registration_data']
The problem is that if I run this, then, has_permission_POST cannot get the value of request.data, which contains the data posted from my frontend. I can work around this, by adding the #api_view(['GET', 'POST']) decorator to has_permission_POST. But that introduces another error, a failed assertion:
AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'bool'>`
This happens from has_permission_POST once it is decorated with #api_view.
So my problems:
How to implement a custom authentication for my use case?
If I am going about this right, by using this custom has_permission_level, how can I get the request.data in this function before my actual api view is called, so that I can read the clinic id and do the checks for permission that I need.
I have taken a look at the CustomAuthentication provided by DRF, but could not find out how to get the request.data parameters in the custom class.
Thanks to #MihaiChelaru, I was able to find a solution to my problem.
I created a custom Permission class by extending permissions.BasePermission, and using my custom logic in the special has_permission function. I went a step further and implemented checking of Token from the request. Once token is authenticated, the user can be got from the matching token from the Token table. I found that in the custom permission class, I could read the full request.data paramter passed by Vue and Postman. Once I read that, I could easily implement the custom checking of User permissions that my custom models had.
class CustomerAccessPermission(permissions.BasePermission):
message = 'No permission to create new patient records'
def has_permission(self, request, view):
bearer_authorizn = request.META.get('HTTP_AUTHORIZATION')
try: #Different apps like POSTMAN, and Vue seem to use different strings while passing token
token = bearer_authorizn.split("Bearer ")[1]
except Exception as e:
try:
token = bearer_authorizn.split("Token ")[1]
except Exception as e:
raise NotAuthenticated('Did not get token in request')
try:
token_obj = Token.objects.get(key=token)
except self.model.DoesNotExist:
raise AuthenticationFailed('Invalid token')
if not token_obj.user.is_active:
raise AuthenticationFailed('User inactive or deleted')
print("Username is %s" % token_obj.user.username)
print("POST details", request.data)
linkedclinic_id = request.data['data']['linkedclinic']
clinic = Clinic.objects.get(clinicid=int(linkedclinic_id))
print("Clinic membership requested:", clinic)
group_maps = UserGroupMap.objects.filter(user=user, clinic=clinic)
print(f"No: of UserGroupMap memberships: {len(group_maps)}")
if len(group_maps) > 1:
return True
return False
#api_view(['POST'])
#permission_classes([CustomerAccessPermission])
def register_patient_vue(request):
logger.info('In register_patient_vue...')
...
I just got my hands on an user account create view that looks like this:
#login_required
def user_create(request):
template_name = 'user/User_Create.html'
if request.method == 'POST':
#this part is not important
pass
else:
form = UserCreateForm()
user_error = ''
context = {'form': form, 'user_error': user_error}
return render(request, template_name, context)
with the UserCreateForm written like this:
class UserCreateForm(forms.ModelForm):
def save(self, commit=True):
user = super(UserCreateForm,self).save(commit=False)
username = self.cleaned_data['username']
username = username.replace(".", "")
username = username.replace("-", "")
user.username = username
if commit:
user.save()
return user
class Meta:
model = User
fields = ['username', 'name', 'profile', 'redefine_password', 'name_created']
widgets = {
'username': forms.TextInput(),
'name': forms.TextInput(),
'profile': forms.Select(),
'redefine_password': forms.CheckboxInput(),
'name_created': forms.TextInput(),
}
My problem is that we have different types of users(Admin, Supervisor, Support, Normal) and currently, Supervisors are able to create Admin accounts...
My initial approach was to pass the user from the view to the form, like this:
form = UserCreateForm(user=request.user)
and in the form, I'm trying to delete the option if the user is not an Admin, like this:
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(UserCreateForm, self).__init__(*args, **kwargs)
if not user.is_superuser:
del self.fields['profile'][1, 'Administrador']
but that failed miserably, I got a TypeError: 'ModelChoiceField' object does not support item deletion.
I tried assigning it to None but that didn't work as well since it doesn't support item assignment neither.
Lastly, I tried assisgning it to a forms.ModelChoiceField() using the queryset attribute but I couldn't make it work.
Could someone shed a light?
Edit:
What I am trying to do is to remove the option to create an admin account in case the current logged user is not an admin, the option is defined in the profile choices.
I have implemented Django-allauth using Facebook as a social account provider as it gives lots of information about its users.
Below is my custom signup form:
ACCOUNT_SIGNUP_FORM_CLASS = 'profiles.forms.MySignupForm'
class MySignupForm(forms.ModelForm):
class Meta:
model = get_user_model()
fields = ['email', 'first_name', 'last_name']
def __init__(self, *args, **kwargs):
super(MySignupForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.fields["email"].widget.input_type = "email" # ugly hack
self.helper.form_method = "POST"
self.helper.form_action = "account_signup"
self.helper.form_id = "signup_form"
self.helper.form_class = "signup"
self.helper.layout = Layout(
Field('email', placeholder="Enter Email", autofocus=""),
Field('first_name', placeholder="Enter First Name"),
Field('last_name', placeholder="Enter Last Name"),
Field('password1', placeholder="Enter Password"),
Field('password2', placeholder="Re-enter Password"),
Submit('sign_up', 'Sign up', css_class="btn-warning"),
)
def signup(self, request, user):
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.email = self.cleaned_data['email']
user.save()
*What else can be added here in signup form ??? or to do the processing on received input i need to write adapter ??*
I have below signal receiver implemented.
#receiver(user_signed_up)
def set_initial_user_names(request, user, sociallogin=None, **kwargs):
if sociallogin:
***grab the data***
email_verified = sociallogin.account.extra_data['verified']
profile = models.Profile(user=user, avatar_url=picture_url, email_verified=email_verified)
profile.save() <--- *saving custom user profile here*
from allauth.account.models import EmailAddress
emails = EmailAddress.objects.filter(user=user, email=user.email)
for email in emails:
email.verified = email_verified
email.save() <--- *saving allauth Email Address instance.*
user.guess_display_name()
user.save() <----- *saving custom user model based on email address.*
Now if you see i am verifying if email is verified by social account(Facebook) if so i am updating Email Address instance of allauth.
But it happens twice.
allauth already updates the email address instance (account_emailaddress) during the login/sign up process. So database is getting hit twice for account_emailaddress table.
I want to control this scenario myself so that it only does it once..
Answer is Adapter, but if i write adapter, what happens to the signal receiver ??
Can someone tell me the adapter/signal flow ?? how it should be implemented. ??
I have gone through the documentation but still it would be nice if i get some direction on the flow like at which stage what i can control !!
Don't use a ModelForm for your custom signup form, as allauth needs to be in charge of constructing a User instance and saving it. Make it a plain form, simply deriving from django.forms.Form. Only use it in cases when you need to add additional inputs during signup. Looking at your form it seems you are not adding any additional fields, so you probably don't need a custom form at all.
Use the following form to ask users for their first/last name:
class SignupForm(forms.Form):
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=30)
def signup(self, request, user):
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.save()
I'm using python-social-auth to log users in to my site, which works fine but I want to use a custom user model that will not only save basic info about the user, but also gets their profile picture.
Here is my user model
def get_upload_file_name(instance, filename):
return "%s_%s" % (str(time()).replace('.', '_'), filename)
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
name = models.CharField(max_length=250, null=True, blank=True)
profile_image = models.ImageField(upload_to = get_upload_file_name, null=True, blank=True)
def __str__(self):
return u'%s profile' % self.user.username
This is the pipeline function
def user_details(strategy, details, response, user=None, *args, **kwargs):
if user:
if kwargs['is_new']:
attrs = {'user': user}
if strategy.backend.name == 'facebook':
fb = {
'name': response['first_name']
}
new_user = dict(attrs.items() + fb.items())
UserProfile.objects.create(
**new_user
)
elif strategy.backend.name == 'google-oauth2':
new_user = dict(attrs.items())
UserProfile.objects.create(
**new_user
)
elif strategy.backend.name == 'twitter':
new_user = dict(attrs.items())
UserProfile.objects.create(
**new_user
)
And this is the other function that gets the user profile image
def save_profile_picture(strategy, user, response, details, is_new=False,
*args, **kwargs):
if is_new and strategy.backend.name == 'facebook':
url = 'http://graph.facebook.com/{0}/picture'.format(response['id'])
try:
response = request('GET', url, params={'type': 'large'})
response.raise_for_status()
except HTTPError:
pass
else:
S_user = setattr(UserProfile, "profile_image", "{0}_social.jpg".format(user.username), ContentFile(response.content))
S_user.save()
I'm only trying it on facebook first, but I can't seem to populate the name field in the database, and I also have to sign in twice before it gets saved to the default social-auth table. Both functions have been added to the settings.py file, I was also wondering if it matters where they go in the cue if it matters since they're at the bottom, the last part of the auth process?
I figured it out, since i was using python3 i should of used list() on my dict values like so: attrs = dict(list(attrs.items()) + list(fb_data.items()))
Also instead of saving the image in the database it was best just to save the url, saving alot of space
I'm using Django-Profiles with Django 1.4, and I need a way to unsubscribe a user, so they can stop getting emails.
One of the fields in my UserProfile model is user_type, and I have a USER_TYPES list of choices. To keep users in the system, even if they unsubscribe, I decided to have one of the USER_TYPES be InactiveClient, and I'd include a checkbox like so:
Models.py:
USER_TYPES = (
('Editor', 'Editor'),
('Reporter', 'Reporter'),
('Client', 'Client'),
('InactiveClient', 'InactiveClient'),
('InactiveReporter', 'InactiveReporter'),
)
class UserProfile(models.Model):
user = models.OneToOneField(User, unique=True)
user_type = models.CharField(max_length=25, choices=USER_TYPES, default='Client')
... etc.
forms.py
class UnsubscribeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UnsubscribeForm, self).__init__(*args, **kwargs)
try:
self.initial['email'] = self.instance.user.email
self.initial['first_name'] = self.instance.user.first_name
self.initial['last_name'] = self.instance.user.last_name
except User.DoesNotExist:
pass
email = forms.EmailField(label='Primary Email')
first_name = forms.CharField(label='Editor first name')
last_name = forms.CharField(label='Editor last name')
unsubscribe = forms.BooleanField(label='Unsubscribe from NNS Emails')
class Meta:
model = UserProfile
fields = ['first_name','last_name','email','unsubscribe']
def save(self, *args, **kwargs):
u = self.instance.user
u.email = self.cleaned_data['email']
u.first_name = self.cleaned_data['first_name']
u.last_name = self.cleaned_data['last_name']
if self.unsubscribe:
u.get_profile().user_type = 'InactiveClient'
u.save()
client = super(UnsubscribeForm, self).save(*args,**kwargs)
return client
Edit: I've added additional code context. if self.unsubscribe: is in save() override. Should that be somewhere else? Thank you.
Edit2: I've tried changing UnsubscribeForm in several ways. Now I get a 404, No User matches the given query. But the view function being called works for other forms, so I'm not sure why?
urls.py
urlpatterns = patterns('',
url('^client/edit', 'profiles.views.edit_profile',
{
'form_class': ClientForm,
'success_url': '/profiles/client/edit/',
},
name='edit_client_profile'),
url('^unsubscribe', 'profiles.views.edit_profile',
{
'form_class': UnsubscribeForm,
'success_url': '/profiles/client/edit/',
},
name='unsubscribe'),
)
These two urls are calling the same view, just using a different form_class.
Edit3: So I don't know why, but when I removed the trailing slash from the unsubscribe url, the form finally loads. But when I submit the form, I still get an error: 'UnsubscribeForm' object has no attribute 'unsubscribe' If anyone could help me understand why a trailing slash would cause the 404 error (No User matches the given query) I wouldn't mind knowing. But as of now, the form loads, but doesn't submit, and the trace ends on this line of my form:
if self.unsubscribe:
Answering my own question again. On ModelForms, you can add form elements that don't exist in the model, and access the value of those fields by accessing self.cleaned_data['form_element_name'] in the save method.
This is what my save method looks like:
def save(self, *args, **kwargs):
u = self.instance.user
p = self.instance.user.get_profile()
u.email = self.cleaned_data['email']
u.first_name = self.cleaned_data['first_name']
u.last_name = self.cleaned_data['last_name']
if self.cleaned_data['unsubscribe']:
p.user_type = 'InactiveClient'
u.save()
p.save()
client = super(UnsubscribeForm, self).save(*args,**kwargs)
return client