Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python A Social Network with Flask How to Win Friends Suggestions

Andy Hughes
Andy Hughes
8,478 Points

Time to tackle groups of users! But what's the best approach?

For the social app, in the suggestions video, Kenneth talks about adding user groups. I am about to start trying this out but before I take a long walk down the wrong road, I thought I'd seek some counsel.

1) My thinking is that it makes sense for any user to use a single login, rather than have a login for this group and a login for that group. So I would see it as any user signs in and the sign in function then figures out if you belong to one group or another.

2) I am also thinking that my user groups should be a separate table but not entirely sure. For example, if I had a bunch of students and these are users, then I had teachers who are also users but have different needs like seeing all of their students, setting homework etc. So would you recommend separating these into separate tables as I'm suggesting? Or would you have everbody as users in one table and then have some sort of table field that identifies different groups? Or would you do it different to either of the above?

From number 1), I can already see a problem with the existing login function if I want the single sign in function for all groups of users. The existing function only checks a user against the 'User' table so if I have a 'User' table and a 'Teacher' table, how would I modify the function to check for both?

I've already started playing around trying to see if I can make it work but obviously I'm still too new and it's eluding me. Below is the login function with my adaptions to try and check both tables. The error I get is 'There is a problem' so it's getting stuck right at the start.

Any feedback is gratefully received. :)

app.py

@app.route('/login', methods=('GET', 'POST'))
def login():
    form = forms.LoginForm()
    if form.validate_on_submit():
        try:
            user = models.User.get(models.User.email == form.email.data) or models.Instructor.get(models.Instructor.email == form.email.data)
        except models.DoesNotExist:
            flash("There was a problem", "error")
        else:
            if check_password_hash(user.password, form.password.data):
                login_user(user)
                if (user.is_admin == 1):
                    return redirect(url_for('admin'))
                elif (models.Instructor.email == form.email.data):
                    return redirect(url_for('instructor_profile'))
                else:
                    flash("You've been logged in!", "success")
                return redirect(url_for('index'))
            else:
                flash("Your email or password doesn't match!", "error")
    return render_template('login.html', form=form)

4 Answers

Do you have overlap between the Users and the Instructors, or are they distinct? If someone is a student in one class and an instructor in another, it might be easier to have one table for basic user information in addition to the User and Instructor tables, with a foreign key to link the tables, so that when information like name or email addresses are updated, they only need to be updated once and not once for each table. If no one is both a student and an instructor, having two distinct tables with different information stored in them seems reasonable.

Instead of

        try:
            user = models.User.get(models.User.email == form.email.data) or \
                models.Instructor.get(models.Instructor.email == form.email.data)
        except models.DoesNotExist:
            flash("There was a problem", "error")

which will throw an exception if the email does not match a User's email, you might want something like

        try:
            user = models.User.select().where(models.User.email == form.email.data).first() or \
                models.Instructor.select().where(models.Instructor.email == form.email.data).first()
            if not user:
                raise models.DoesNotExist
        except models.DoesNotExist:
            flash("There was a problem", "error")

models.User.get(models.User.email == form.email.data) is similar to models.User.select().where(models.User.email == form.email.data), but while .get() returns a single row for the result set or throws an exception if no results are found, .select().where() returns all the qualifying rows in its result set with a possible empty result set. Assuming that emails are unique, you can use .first() to get only the first row in the result set or None if the email was not found. You can then raise an exception if user is None.

In the line if (user.is_admin == 1):, what does 1 mean? If user.is_admin is a boolean, you could replace the if statement with if user.is_admin:.

In the line elif (models.Instructor.email == form.email.data):, models.Instructor.email is probably something like <CharField: Instructor.email> rather than an email address. You could replace the condition with models.Instructor.select().where(models.Instructor.email == form.email.data).first() to determine if the email from the form matches an instructor's email, which would be repeating code from the try block. You could also consider setting a variable in the try block to keep track of if the email was for an instructor, such as

        try:
            is_instructor = models.Instructor.select().where(models.Instructor.email == form.email.data).first()
            user = models.User.select().where(models.User.email == form.email.data).first() or is_instructor  
            if not user:
                raise models.DoesNotExist
        except models.DoesNotExist:
            flash("There was a problem", "error")

then using elif is_instructor:.

Andy Hughes
Andy Hughes
8,478 Points

Hey jb30 :), thank you again as your answer really helps my thinking along.

I do see the Users and Instructors as separate groups, i.e. one cannot be part of the other. Students will be taught by Instructors, but Instructors cannot be students. So, I think it does make sense to keep them in different tables and relate them with a foreign key, yes?

I also realised that where Kenneth has built an option for a User to be admin, but I think a student is unlikely to be an admin. So, with all that said, I see three distinctly separate groups:

Users Instructors Admins (these might be venues for example that might arrange or setup courses)

Given the above, it might be better for me to completely rewrite the login function rather than trying to modify the existing one.

Your comment has got me thinking that the variable for storing the 'User', 'Instructor', 'Admin' makes sense and then checking the login form for one of these conditions. So I've had a play around with the original function (trying before I attempt to rewrite the entire thing!) based on your feedback. It now seems to get past the first try block but is throwing the message 'please login to access this page'. This message is not in my code so I think it is a Peewee message from the attempt to 'login_user(user)'?

Interestingly though, my url shows the following:

http://localhost:8000/login?next=%2Finstructor_profile

According to the flask-login docs, this would suggest that the user isn't logged in. Or maybe it's trying to reconcile 'User' with an 'Instructor'. I'm not entirely sure but I think it's a small step forward.

Give me a bit more time to play around with it and take in more of your feedback :) Thanks again

Andy Hughes
Andy Hughes
8,478 Points

jb30 - Having looked at the docs and scoured stackoverflow, I think I see a challenge of using flask-login. According to the docs, the way to allow for multiple types of users is with 'roles'. So you have one User class that has a column for 'role' and within that, you define a tuple that is along the lines of ('role', 'need'). A bit confusing here but essentially 'role' would actually be just that a role. The need is then what kind of role i.e. 'User', 'Instructor' or 'admin'.

With the above in mind, the 'login_user(user)' would then still just login the user (because they are part of the user table)) and you would then have a function that uses the role to apply permissions for access to different information.

The problem with the existing structure of code is the 'login_user(user)'. There is no way I can see at the moment to tell flask-login that 'user' could also be an instructor.

Interested in your thoughts. Does the above sound correct, or do you think it's still possible to do a single login whilst still using flask-login?

Loving the challenge, but I fear I am confusing myself further each day! :P

While it does not appear to be best practice, it does appear to be possible.

In your user_loader callback, you can use the user_id parameter to determine which model to access. Assuming that you only have two classes of models, User and Instructor,

@login_manager.user_loader
def load_user(user_id):
    if user_id % 2 == 0:
        return models.User.get_or_none(id=user_id/2)
    else:
        return models.Instructor.get_or_none(id=(user_id - 1)/2)

In the User class,

def get_id(self):
    return self.id * 2

In the Instructor class,

def get_id(self):
    return self.id * 2 + 1

The User class will then give flask-login an even number from get_id(), and the Instructor class will give flask-login an odd number from get_id(). flask-login uses the id from get_id() in load_user(). Since the id in the User class was doubled to get the value for get_id(), the value of the id in the User class is half of the value of user_id. Since the id in the Instructor class was doubled and had 1 added to it to get the value for get_id(), the value of the id in the User class is half of the value of 1 less than the user_id.

get_or_none is used so that load_user will return either an instance of the specified model or None.

If you have three classes of models, User, Instructor, and Admin, the logic is similar. Instead of even (user_id % 2 == 0) and odd (user_id % 2 == 1), you would have user_id % 3 == 0, user_id % 3 == 1, and user_id % 3 == 2 in load_user(), and self.id * 3, self.id * 3 + 1, and self.id * 3 + 2 in get_id().

Andy Hughes
Andy Hughes
8,478 Points

jb30 - So in between me writing and you answering (thank you as always, I owe you a drink!), I decided to go with the single User model. I will have all users in the same table with a role column.

I've tested it and know that the user registration works and populates all the fields correctly. When the user tries to log-in, it allows them to but I now have the same problem I originally had; how do I identify the user logging in against the role in the User table and then send them to the right profile page. My thinking is something along these lines:

''' login users '''
@app.route('/login', methods=('GET', 'POST'))
def login():
    form = forms.LoginForm()
    if form.validate_on_submit():
        try:
            user = models.User.select().where(models.User.email == form.email.data).first()
            if not user:
                raise models.DoesNotExist
        except models.DoesNotExist:
            flash("There was a problem", "error")
        else:
            if check_password_hash(user.password, form.password.data):
                login_user(user)
                if user.is_admin:
                    return redirect(url_for('admin'))
                elif models.User.role == 'instructor':
                    return redirect(url_for('instructor_profile'))
                else:
                    flash("You've been logged in!", "success")
                return redirect(url_for('profile'))
            else:
                flash("Your email or password doesn't match!", "error")
    return render_template('login.html', form=form)

In my logic, I think the above code should catch any user and log them in. What it should then do is check if they are admin (redirect to admin page), if not, it then checks for the users role (redirects them to instructor profile), else it redirects to user profile.

It's the Instructor part that feels wrong to me. I guess models.User.role == 'instructor' doesn't know which user we're talking about? Maybe after selecting the user through their email, I check for the role in try block and store that in a variable?

Try changing elif models.User.role == 'instructor': to elif user.role == 'instructor':

Andy Hughes
Andy Hughes
8,478 Points

jb30 - Sometimes, it's the simplest and most frustrating thing!! I tried your code above, which makes sense now as user is declared earlier in the function so it's already referencing the user table. But it still didn't work!

Everything kept redirecting back to the same profile page regardless of the role! I then wondered if it might be a problem with the column itself (maybe I named it differently or something). I checked the column and realised that the data being passed to the table is Instructor and not instructor as per my function. Once I changed that, with your last comment above, it worked just as expected.

Thanks again. The conversation and working through the logic of your answers has really helped my understanding!

I owe ya! :)