diff --git a/README.md b/README.md index 366f80c..3d1cc63 100644 --- a/README.md +++ b/README.md @@ -108,4 +108,23 @@ If you see any bugs, please tell me. I really tried my best by testing every fea ## Django model -I'm using django model a little in my code only to create me some "barriers". this standardize my objects from redis to avoid lot's of bugs. This is only kinda sort of structure to format my data. \ No newline at end of file +I'm using django model a little in my code only to create me some "barriers". this standardize my objects from redis to avoid lot's of bugs. This is only kinda sort of structure to format my data. + +## Pub/Sub + +To implement the Redis pub/sub in Django, I decided to use this mechanism: + +- The `notifications_view` is firstly displayed + +- This view calls the `notifications` function thanks to HTML and JS + +- This calls the get_message function, waiting for a new message + +- While waiting this message, the HTMl refreshs the page every 10s to avoid the timeout + +- When a message is received, `notifications` function calls back `notifications_view` and this repeats. + +It stops when the user change page. So, the page is constantly "loading" waiting for a message. This allowed me to get real-time message. It's not the most efficient +way to do this but I really didn't want to use websockets or JS libraries to do this. It seems the fastest easiest way to do it. + +Also, I saw in some docs we could perform some async functions in Django, but seems kinda long and heavy to implement, so I choose only this method. \ No newline at end of file diff --git a/redis_app/utils/model.py b/redis_app/utils/model.py index 3bc3b61..b0ab558 100644 --- a/redis_app/utils/model.py +++ b/redis_app/utils/model.py @@ -269,10 +269,10 @@ def refresh_expire(course_id): def getCoursesFromPerson(person_id): # Getting all of courses keys (not into person object because even if we use redis, which is a NoSQL technology, I really wanted to # stay the most consistent I could by not having two lists for the same data. For example, the courses has a students list. If the students would have - # a courses list, the data would have been duplicated, for no reason except the performance. I decided not to take the risk to duplicate this data + # a courses list, the data would have been duplicated, for no reason except the efficience. I decided not to take the risk to duplicate this data # because I strongly believe that it will stay optimized enough to iterate over all courses. Also, I decided to put the list for the courses because # generally there will be more students than courses (because for one course, there's around 30 students), so iterate over the persons (students) would have - # been less performant than over courses. This is only a choiceof implementation. I could obviously be wrong. But that's my choice, and of course this choice + # been less efficient than over courses. This is only a choiceof implementation. I could obviously be wrong. But that's my choice, and of course this choice # could change at the same time as my project lives) course_keys = redis_connection.keys(f"course:*") courses = [] diff --git a/redis_app/views.py b/redis_app/views.py index a9308a0..2569396 100644 --- a/redis_app/views.py +++ b/redis_app/views.py @@ -1,4 +1,7 @@ +# To render my HTML with arguments from django.shortcuts import render + +# It's faster to return some 404 when an object does not exist from django.http import Http404 # This corresponds to all CRUD and Utils operation associating with redis. This has to be in a different file to separate view logic and CRUD logic. @@ -7,266 +10,551 @@ from redis_app.utils.model import * # This model only helps us the development part to set rules on objects (we do not use the django model to save objects in it) from .models import Person +# Each function here is associated to an URL as we defined it in urls.py. They take an HttpRequest as parameter and return an HttpRequest. + +# Home page, giving the logged in person def home(request): + # Getting the logged in person person = request.session.get("person", "") return render(request, 'home.html', {'person': person}) +# Detaisl of a course. Taking a course_id (by parameter of the route) and may take an error message and/or a success message to allow other function to call it easily def details(request, course_id, error_message='', success_message=''): + # Getting the logged in person person = request.session.get("person", "") + + # If the course id expired, 404. if isExpired(course_id): raise Http404("Course has expired... Get faster next time!") + + # Getting the course course = getCourse(course_id) if not course: + # Does not exist. 404. raise Http404("Course does not exist") + + # Setting up the course id into the course manually course['id'] = course_id + + # Getting the teacher of the course teacher = getPerson(course['teacher']) if not teacher: + # Does not exists. 404. raise Http404("Teacher does not exist") + + # Getting the teacher's name to display it into the view course['teacher_name'] = teacher['name'] + + # Giving the bool to know if the logged in person is registered in this course register = isPersonRegisteredToCourse(course, person) + + # Geving the bool to know if the course is full full = isCourseFull(course) + + # Rendering the HTML with all these information return render(request, 'details.html', {'course': course, 'person': person, 'register': register, 'full': full, 'error_message': error_message, 'success_message': success_message}) +# Called after filled the register form def register(request): + # Only handling POST request if request.method == 'POST': + # Trying to register person = register_redis(request.POST['name'], request.POST['role']) if not person: + # Already exists if we have a false return render(request, 'register.html', {'person': Person(name="", role=""), "error_message": "This user already exists!"}) + + # Successfully registered + # Setting up the person to the session request.session["person"] = person + # Go to home return home(request) + # Not a POST. Try again buddy. return render(request, 'register.html', {'person': Person(name="", role=""), "error_message": "The form has been wrongly filled!"}) +# Called after filled the login form def login(request): + # Only handling POST request if request.method == 'POST': + # Trying to login person = login_redis(request.POST["name"], request.POST["role"]) if not person: + # The user does not exist if we have a false return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "No user found!"}) + + # Successfully logged in + # Setting up the person to the session request.session["person"] = person + # Go to home return home(request) + + # Not a POST. Try again buddy. return login_form(request) +# Displaying the register form def register_form(request): return render(request, 'register.html', {'person': Person(name="", role="")}) +# Displaying the login form def login_form(request): return render(request, 'login.html', {'person': Person(name="", role="")}) +# Go back to login and remove the login person from the session def logout(request): request.session["person"] = "" return render(request, 'login.html', {'person': Person(name="", role="")}) +# Displaying the courses page of a person def courses(request, error_message="", success_message=""): + # Getting the logged in person person = request.session.get("person", "") if person == "": + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Getting up the person id (only the id part without person:) person_id = getPersonId(person).split(":")[1] + # Getting all courses of the person (if he's a teacher, taking all courses where he's teacher ; if he's a student, taking all courses where he's registered to) courses = getCoursesFromPerson(person_id) return render(request, 'courses.html', {'person': person, 'courses': courses, 'error_message': error_message, 'success_message': success_message}) +# Called after filled the update profile form def change_profile(request): + # Getting the logged in person person = request.session.get("person", "") if person == '': + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + # Only handling POST request if request.method == 'POST': + # Getting the person id person_id = getPersonId(person) if not person_id: + # Not found return render(request, 'profile.html', {'person': person, "error_message": "Internal error, person id not found!"}) + + # Changing the profile with new name and role requested person = changeProfile_redis(person_id, request.POST['name'], request.POST['role']) if not person: + # We got a false, so this username with this role is already taken. return render(request, 'profile.html', {'person': person, "error_message": "This username with this role is already taken!"}) + + # Setting up the new person into the session request.session["person"] = person return render(request, 'profile.html', {'person': person, 'success_message': 'Your profile has been successfully changed!'}) + # Not a POST. Try again buddy. return login_form(request) +# Go to profile page def profile(request): + # Getting the logged in person person = request.session.get("person", "") if person == '': + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) return render(request, 'profile.html', {'person': person}) +# After clicked on the button delete of a course def delete_course_view(request, course_id): + # Checking if the course is expired if isExpired(course_id): + # Course is expired (or does not exist) raise Http404("Course has expired... Get faster next time!") + + # Tries to delete the course res = delete_course(course_id) if not res: - return courses(request) + # Didn't delete the course, internal error + return courses(request, "Internal error: Can't delete the course.") + + # Publishing the delete of the course to all students registered to it (if it's their only course where they're registered to, it make go + # them back to courses to let them register to another course before getting access back to notifications page, because they're not registered to any course now) publish(course_id, f"DELETE: Course:{course_id} has been deleted!") return courses(request) +# Accessing to the publish message page def publish_message(request, success_message=""): + # Getting the logged in person person = request.session.get("person", "") - courses_fetched = getCoursesFromPerson(getPersonId(person).split(":")[1]) + if person == '': + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Getting person id + person_id = getPersonId(person) + if not person_id: + # Person id not found, internal error + return render(request, 'publish_message.html', {'person': person, 'courses': [], 'error_message': 'Internal error: Person id not found.'}) + + # Getting courses from person (by spliting it to only get the id part, withot person:) + courses_fetched = getCoursesFromPerson(person_id.split(":")[1]) + + # For each course, we add manually the id to it (without course:, by spliting it) for course in courses_fetched: course['id'] = getCourseId(course).split(':')[1] + + # If courses_fetched is empty, can't publish a message because a message published is associated to a course, so he must create a course before publishing a message if not courses_fetched: + # No course return courses(request, 'You don\'t have any course. You must create one to publish a message!') + + # Retrieved all of his courses return render(request, 'publish_message.html', {'person': person, 'courses': courses_fetched, 'success_message': success_message}) +# Called after filled the publish message form def publish_message_form(request): + # Getting the logged in person person = request.session.get("person", "") if person == '': + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Only for teacher. if person['role'] == 'Student': return render(request, 'publish_message.html', {'person': person, "error_message": "The form has been wrongly filled!"}) + + # Only handling POST request if request.method == 'POST': + # Getting information from the form course_id = request.POST['course_id'] message = request.POST['message'] + + # Getting the course id course = getCourse(course_id) if not course: + # Course not found. 404. raise Http404("Course not found") + + # Publishing the message into the course_id channel publish(course_id, message) return publish_message(request, 'The message has been successfully sent!') - + + # Not a POST. Try again buddy return render(request, 'publish_message.html', {'person': person, "error_message": "The form has been wrongly filled!"}) +# Displaying the create course form def create_course_view(request): + # Getting the logged in person person = request.session.get("person", "") + if person == '': + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + return render(request, 'create.html', {'person': person}) +# Displaying the update course form def update_course_view(request, course_id): + # Getting the logged in person person = request.session.get("person", "") + if person == '': + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Checking if the course is expired if isExpired(course_id): raise Http404("Course has expired... Get faster next time!") + + # Getting the course course = getCourse(course_id) if not course: + # Course not found. 404. raise Http404("Course does not exist") + + # Getting course id course_id = getCourseId(course) if course_id == False: + # Course id not found. 404. raise Http404("Course does not exist") + + # Setting up manually the course id into the course course['id'] = course_id.split(":")[1] return render(request, 'update.html', {'person': person, 'course': course}) +# Called after filled the create course form def create_course_form(request): + # Getting the logged in person person = request.session.get("person", "") if person == '': + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Only handling POST request if request.method == 'POST': + # Getting information from the form course_title = request.POST['title'] course_summary = request.POST['summary'] course_level = request.POST['level'] course_places = request.POST['places'] - course_teacher = getPersonId(person).split(":")[1] + + # Getting the person id + person_id = getPersonId(person) + if not person_id: + # Person id not found. 404. + raise Http404("Person id not found") + + # Course teacher is the person id without person: + course_teacher = person_id.split(":")[1] + + # Creating the course with all these information create_course(course_title, course_summary, course_level, course_places, course_teacher) return courses(request, "", "The course has been successfully created!") + # Not a POST. Try again buddy; return render(request, 'create.html', {'person': person, "error_message": "The form has been wrongly filled!"}) +# Called after filled the update course form def update_course_form(request, course_id): + # Getting the logged in person person = request.session.get("person", "") - if isExpired(course_id): - raise Http404("Course has expired... Get faster next time!") if person == '': + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Checking if the course is expired + if isExpired(course_id): + raise Http404("Course has expired... Get faster next time!") + + # Only handling POST request if request.method == 'POST': + # Retriving the POST information course_title = request.POST['title'] course_summary = request.POST['summary'] course_level = request.POST['level'] course_places = request.POST['places'] - course_teacher = getPersonId(person).split(":")[1] + + # Getting the person id + person_id = getPersonId(person) + if not person_id: + # Person id not found. 404. + raise Http404("Person id not found") + + # Course teacher is the person id without person: + course_teacher = person_id.split(":")[1] + + # Getting the course from the course id course = getCourse(course_id) + if not course: + # Course not found. 404. + raise Http404("Course id not found") + + # Setting up manually the course id course["id"] = course_id + + # Getting all course students (and "" by default) course_students = course.get("students", "") + + # Converting a string to int, very dangerous so try catch try: + # Checking if the new number of places is not < number of students currently registered if course_students and len(course_students.split(',')) >= int(course_places): + # Too many students registered to change the number of places that low return render(request, 'update.html', {'person': person, "error_message": "There's too many students registered to decrease the number of places that much!", "course": course}) except: + # Number of places not an int return render(request, 'update.html', {'person': person, "error_message": "The number of places has not the right format!", "course": course}) + + # Updates the course update_course(course_id, course_title, course_summary, course_level, course_places, course_teacher) return courses(request, "", "The course has been successfully changed!") + # Not a POST. Try again buddy; return render(request, 'update.html', {'person': person, "error_message": "The form has been wrongly filled!", "course": course}) +# When clicked on register to a course def course_register_view(request, course_id): + # Getting the logged in person person = request.session.get("person", "") + if person == '': + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Checking if the course is expired if isExpired(course_id): + # Course expired. 404. raise Http404("Course has expired... Get faster next time!") + + # Getting the person id person_id = getPersonId(person).split(":")[1] if not person_id: + # Person id not found. 404. raise Http404("Person not found") + + # Getting the course course = getCourse(course_id) if not course: + # Course not found. 404. raise Http404("Course not found") + + # Setting up manually the course id course["id"] = course_id + + # Getting the whole teacher object of the course teacher teacher = getPerson(course['teacher']) if not teacher: + # Teacher not found. 404. raise Http404("Teacher does not exist") + + # Setting up manually the teacher name to the course course['teacher_name'] = teacher['name'] + + # Trying to register the person to the course if course_register(course_id, person_id): + # Registered successfully return details(request, course_id, '', 'Successfully registered to the course.') + + # Couldn't register return details(request, course_id, "Could not register to the course. Try again later.") +# When clicked on unregister from a course def course_unregister_view(request, course_id): + # Getting the logged in person person = request.session.get("person", "") if person == "": + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Checking if the course is expired if isExpired(course_id): + # Course expired. 404. raise Http404("Course has expired... Get faster next time!") + + # Getting the person id person_id = getPersonId(person).split(":")[1] if not person_id: + # Person id not found. 404. raise Http404("Person not found") + + # Getting the course course = getCourse(course_id) if not course: + # Course not found. 404. raise Http404("Course not found") + + # Setting up manually the course id course["id"] = course_id + + # Getting the whole teacher object of the course teacher teacher = getPerson(course['teacher']) if not teacher: + # Teacher not found. 404. raise Http404("Teacher does not exist") + + # Setting up manually the teacher name to the course course['teacher_name'] = teacher['name'] + + # Trying to unregister the person from the course if course_unregister(course_id, person_id): + # Unregistered successfully return details(request, course_id, '', "Successfully unregistered to the course.") + + # Couldn't unregister return details(request, course_id, "Could not unregister to the course. Try again later.") -def search(keywords): - return search_redis(keywords) - +# Displaying the search page def search_view(request): + # Getting the logged in person person = request.session.get("person", "") + if person == "": + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + return render(request, 'search.html', {'person': person}) +# Called after filled the search form def search_form(request): + # Getting the logged in person person = request.session.get("person", "") + if person == "": + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Only handling POST request if request.method == 'POST': + # Retrieving information keywords = request.POST['keywords'] + + # All courses objects courses = [] - courses_id = search(keywords) + + # Getting the courses id of the search + courses_id = search_redis(keywords) + + # Getting the whole course object with their id for course_id in courses_id: + # Getting the course with the id (splitted not to get course:) course = getCourse(course_id.split(':')[1]) if not course: + # Didn't work, continuing not to break the feature continue + + # Setting up manually the id to the course object course['id'] = course_id.split(":")[1] + + # Adding the course to the courses list courses.append(course) return render(request, 'search.html', {'person': person, 'courses': courses}) - + + # Not a POST. Try again buddy. return render(request, 'search.html', {'person': person, "error_message": "The form has been wrongly filled!"}) +# Displaying the notification view def notifications_view(request): + # Getting the logged in person person = request.session.get("person", "") + if person == "": + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Getting the messages received to display them messages = request.session.get("messages", []) return render(request, 'notifications.html', {'person': person, 'messages': messages}) +# This function is instantly called after notifications_view. How does my subscribe system work? Please referr to the README to understand it (cf. Section Pub/sub). def notifications(request): + # Getting the logged in person person = request.session.get("person", "") + if person == "": + # The user is not logged in. Go to login + return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Getting the messages from the session not to lose them when navigating messages = request.session.get("messages", []) if person == "": + # The user is not logged in. Go to login return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) + + # Getting the person id person_id = getPersonId(person) if not person_id: + # Person id not found. return render(request, 'notifications.html', {'person': person, 'messages': messages, 'error_message': 'Internale error: person id not found.'}) + + # Getting the courses id where the person is registered to. courses_id = [course['id'] for course in getCoursesFromPerson(person_id.split(":")[1])] if not courses_id: + # When the person is not registered to any course, ask him to register to some course by redirecting him to courses page + # to make him know he's not registered to any course return courses(request, "You're not registered to any course. Please register to one to follow notifications.") + + # Waiting for the message, for a new notification, subscribe to all of his courses channel. message = get_message(*courses_id) - messages.append(message) - messages.reverse() + + # When a message is received, adding it to the messages list at the beginning to get the latest message at the top of the page, and the oldest at the bottom + messages.insert(0, message) + + # Updating the new messages list to the session to display it to the notifications view. request.session["message"] = messages return notifications_view(request) +# Clearing the notifications (the session messages) def clear_notifications(request): + # Messages from session are now an empty list, clearing the list. request.session["messages"] = [] + # Displaying again the notification view return notifications_view(request) # Every id is only the number, not the course:number or person:number \ No newline at end of file