From 0f9bd19bb1e4062a36aeda05519a72b6c4ebcc60 Mon Sep 17 00:00:00 2001 From: Corentin Date: Fri, 1 Nov 2024 22:41:32 +0100 Subject: [PATCH] Commented model.py --- redis_app/templates/search.html | 1 - redis_app/utils/model.py | 222 +++++++++++++++++++++++++++----- redis_app/views.py | 2 +- 3 files changed, 194 insertions(+), 31 deletions(-) diff --git a/redis_app/templates/search.html b/redis_app/templates/search.html index 77640c6..6a8b271 100644 --- a/redis_app/templates/search.html +++ b/redis_app/templates/search.html @@ -38,7 +38,6 @@ You have to type the exact word to match. You can search for multiples courses by typing multiple keywords separated with spaces. This displays the search for each keyword. It won't try to match for the specific search of two keywords. It will handle one keyword at once. - However, if a course has 'test' as a title and has 'test' in description, the search will display the course 2 times, for each time there's the match. --> diff --git a/redis_app/utils/model.py b/redis_app/utils/model.py index a9f6260..3bc3b61 100644 --- a/redis_app/utils/model.py +++ b/redis_app/utils/model.py @@ -1,118 +1,172 @@ +# To use redis import redis + +# To generate some Id for course and persons import uuid +# To create the index to setup the search feature from redis_app.utils.index import create_index +# Constants +# Will expire after 300s = 5 mins DEFAULT_EXPIRE_TIME = 300 +# The refresh add 60s = 1 min EXPIRE_REFRESH_TIME = 60 +# Setting up the redis connection to localhost!6379 redis_connection = redis.Redis(host='localhost', port=6379, decode_responses=True) + +# Setting up the redis pubsub system pub_sub = redis_connection.pubsub() + +# Creating the index for the search feature create_index(redis_connection) #################################################################################### # - PERSONS - # #################################################################################### +# Allows to get the person with the name and the role (because with a name and a role, the person is unique) def getPersonIdNameRole(name, role): + # Defining the query for Redis Stack query = f"@name:{name} @role:{role}" + # Searching with redis Stack results = redis_connection.execute_command('FT.SEARCH', 'idx:persons', query) + # results[0] contains the number of elements. If it's greater than 0, there's at least a result if results[0] > 0: + # Taking the first result person_key = results[1] return person_key + # No person found return False +# Gets the person id with the person object (which obviously doesn't contain the id) def getPersonId(person): return getPersonIdNameRole(person["name"], person["role"]) +# Gets the whole person object with the person id def getPerson(person_id): return redis_connection.hgetall(f"person:{person_id}") +# Allows a user to register def register_redis(name, role): + # Defining the query for Redis Stack query = f"@name:{name} @role:{role}" + # Searching with redis Stack results = redis_connection.execute_command('FT.SEARCH', 'idx:persons', query) - # First element = number of results + # results[0] contains the number of elements. If it's greater than 0, there's at least a result if results[0] > 0: + # The user already exists. return False + # Creating the person_key person_key = f"person:{get_uuid4()}" + # And assigning correct information redis_connection.hset(person_key, mapping={ 'name': name, 'role': role }) + # Retrieving the user just created (we're sure it's been well created with this approach) return redis_connection.hgetall(person_key) +# Allows a user to login def login_redis(name, role): - query = f"@name:{name} @role:{role}" - results = redis_connection.execute_command('FT.SEARCH', 'idx:persons', query) - # First element = number of results - if results[0] > 0: - person_key = results[1] - return redis_connection.hgetall(person_key) - return False + # We just check if the user exists + return getPersonIdNameRole(name, role) +# Allows to change the profile of a user. person_id = current person id, name = name requested, role = role requested def changeProfile_redis(person_id, name, role): + # Checking if the name and role requested is not already associated to a user new_person_id = getPersonIdNameRole(name, role) if new_person_id: return False + + # Setting the new information redis_connection.hmset(person_id, mapping={"name": name, "role": role}) + # Retrieving the user by spliting the id (because the id is person:id and we only want id) return getPerson(person_id.split(':')[1]) #################################################################################### # - COURSES - # #################################################################################### +# Is a course expired? def isExpired(course_id): + # Checking the time to live of a course ttl = redis_connection.ttl(f"course:{course_id}") + # If the ttl is -2 (key doesn't exist) or 0 (no ttl), the key is expired (or does not exist at all) if ttl == -2 or ttl == 0: # If -1, the key has no expiration return True return False +# Is the course full? Is there a place for a new student? def isCourseFull(course): students = course.get('students', '') # Because it crashes with ['students'], we have to put a default value: '' here if not students: + # No one is registered, and since the minimum of place for a course is 1, it's not full return False try: + # We want an int to compare places = int(course['places']) + + # Checking the length of the course's students list, if it's >= course's number of places if len(students.split(',')) >= places: return True return False except: + # Places are not an int, so it's not full (because I had to choose, I choose that if it's wrongly setup, everybody can just register infinitely). return False +# Gets the id of the whole course object (obviously without the id in it) def getCourseId(course): + # Setting up the query to search with redis Stack query = f"@title:{course['title']} @teacher:{course['teacher']}" + # Searching with Redis Stack results = redis_connection.execute_command('FT.SEARCH', 'idx:courses', query) + + # If the number of results > 0 if results[0] > 0: + # Returning the first course course_key = results[1] return course_key + # No course found return False - +# Gets the whole course object with the course_id def getCourse(course_id): return redis_connection.hgetall(f"course:{course_id}") +# Deleting a course def delete_course(course_id): + # Checking if the course exists course = redis_connection.hgetall(f"course:{course_id}") if not course: + # Does not exist return False + # Deleting it redis_connection.delete(f"course:{course_id}") return True +# Creates a course by taking the title, the summary, the level, the number of places and the teacher id def create_course(course_title, course_summary, course_level, course_places, course_teacher): + # Setting up the course id course_id = get_uuid4() + # Setting the new course into Redis (2 courses can be identical, there's no unicity) redis_connection.hset(f"course:{course_id}", mapping={ "title": course_title, "summary": course_summary, "level": course_level, "places": course_places, - "teacher": course_teacher + "teacher": course_teacher # Id of the teacher without the person:, only the uuid }) + # Expire after DEFAULT_EXPIRE_TIME seconds redis_connection.expire(f"course:{course_id}", DEFAULT_EXPIRE_TIME) +# Updates a course by taking the title, the summary, the level, the number of places and the teacher id def update_course(course_id, course_title, course_summary, course_level, course_places, course_teacher): + # Updating the course into Redis (2 courses can be identical, there's no unicity) redis_connection.hset(f"course:{course_id}", mapping={ "title": course_title, "summary": course_summary, @@ -120,62 +174,90 @@ def update_course(course_id, course_title, course_summary, course_level, course_ "places": course_places, "teacher": course_teacher }) + # Expire after DEFAULT_EXPIRE_TIME seconds redis_connection.expire(f"course:{course_id}", DEFAULT_EXPIRE_TIME) + # Publishing the update to notify every students (and the teacher) publish(course_id, f"UPDATE: Course:{course_id} is now: Title: {course_title}, Summary: {course_summary}, Level: {course_level}, Places: {course_places}, Teacher: {course_teacher}") +# Registering to a course def course_register(course_id, person_id): + # Checking if the course and the person exists course = redis_connection.hgetall(f"course:{course_id}") person = redis_connection.hgetall(f"person:{person_id}") if not course or not person: + # One or both don't exist return False + # Taking the students of the course students = course.get('students', '') + + # Checking if the user is not already registered if person_id in students.split(','): + # He's registered return True - - try: - places = int(course['places']) - if students != '': - students_nb = len(students.split(',')) - if students_nb and students_nb >= places: - return False - except: + + # Checking if the course is full + if isCourseFull(course): return False + # if there's not student registered to the course, adding the person id to it if not students: new_students = person_id else: + # There's at least one other student registered, so we're adding the person id after a coma to separate ids new_students = students + "," + person_id + # Setting the new students list redis_connection.hset(f"course:{course_id}", "students", new_students) + refresh_expire(course_id) return True def course_unregister(course_id, person_id): + # Checking if the course and the person exists course = redis_connection.hgetall(f"course:{course_id}") person = redis_connection.hgetall(f"person:{person_id}") if not course or not person: + # One or both don't exist return False + # Taking the students of the course students = course.get('students', '') + + # Checking if the user is registered if person_id not in students.split(','): + # Not registered, so it's all good return True + # Creating a list of all students by spliting it by the coma students_list = students.split(",") + + # If there's only one person (which must be the person asking to unregister because he's in the list and there's only one person) if len(students_list) == 1: + # The new list is empty new_students = "" else: + # Creating a list of person id separated with comas of all students in the list except the person_id one new_students = ",".join([student for student in students_list if student != person_id]) + # Setting up the new list of students for the course redis_connection.hset(f"course:{course_id}", "students", new_students) return True +# Allows to refresh the course ttl when someone register to the course def refresh_expire(course_id): + # Checking if the course exists course = redis_connection.hgetall(f"course:{course_id}") if not course: + # Does not exist return False + + # Checking the ttl ttl = redis_connection.ttl(f"course:{course_id}") if ttl <= 0: + # ttl is < 0 so it's expired, or does not exist at all, or has an infinite ttl return False + + # Adding to the current ttl EXPIRE_REFRESH_TIME seconds for the course_id redis_connection.expire(f"course:{course_id}", ttl + EXPIRE_REFRESH_TIME) return True @@ -183,76 +265,165 @@ def refresh_expire(course_id): # - LINKS - # #################################################################################### +# Gets the courses of a person by its 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 + # 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 + # could change at the same time as my project lives) course_keys = redis_connection.keys(f"course:*") courses = [] + + # Checking if the person exists person = redis_connection.hgetall(f"person:{person_id}") + if not person: + # Does not exist, returning an empty array + return [] + + # For each course key in Redis for key in course_keys: + # Getting the data of the course key course_data = redis_connection.hgetall(key) + + # When the user is a student if person['role'] == "Student": + # We're only interested about courses where the student is if person_id in course_data.get('students', '').split(","): + # Getting the course id course_id = getCourseId(course_data) if course_id == False: + # Didn't work, continuing not to break the feature continue + + # Adding the id manually the course object course_data['id'] = course_id.split(":")[1] + + # Adding the course object to the courses list we will return courses.append(course_data) + + # When the user is a teacher else: + # taking the teacher id of the courses teacher_id = course_data["teacher"] + + # We're only interested in courses where the teacher is the teacher of the course if teacher_id == person_id: + # Getting the course idc course_id = getCourseId(course_data) if course_id == False: + # Didn't work, continuing not to break the feature continue + + # Adding the id manually the course object course_data['id'] = course_id.split(":")[1] + + # Adding the course object to the courses list we will return courses.append(course_data) + + # Returning the courses found return courses +# Is the person registered to the course? def isPersonRegisteredToCourse(course, person): - if not person: + # Checking if the person or the course is not False, or "" + if not person or not course: + # Not a person or a course return False + # Getting the person id person_id = getPersonId(person).split(":")[1] if not person_id: + # Not found return False + # Getting the course id course_id = getCourseId(course) if not course_id: + # Not found return False + # Iterating over each course keys in Redis course_keys = redis_connection.keys(f"course:*") for key in course_keys: + # Getting the course data course_data = redis_connection.hgetall(key) + + # If the person is a student (because a teacher can't register to a course) if person['role'] == "Student": + # And if the person is in the students list of the course if person_id in course_data.get('students', '').split(","): + + # Getting the course id of the current course current_course_id = getCourseId(course_data) if current_course_id == False: + # Didn't work, continuing not to break the feature continue + # Checking if the current course id equals the course id requested if course_id == current_course_id: + # The student is in the current course and the current course has the same id as the course id requested return True + # After iterating over all keys, the students is not into the course_id return False #################################################################################### # - UTILS - # #################################################################################### +# Creating an uuid def get_uuid4(): + # We remove the - because it creates a lot of problems into redis for the search feature return str(uuid.uuid4()).replace('-', '') +# Publish a message for a course def publish(course_id, message): + # Publishing the message "message" into the channel course_id redis_connection.publish(course_id, message) +# Subscribe to courses id, to get a message +def get_message(*courses_id): + # Unsubscribe to everything in case there's still some subscribe before + pub_sub.punsubscribe() + # Subscriing to the whole list of courses id (which corresponds to channels) + pub_sub.psubscribe(*courses_id) + + # Listening for new messages + for message in pub_sub.listen(): + # Checking if the message if a message as we published + if message["type"] != 'pmessage': + # Not a message that is interesting for us + continue + # Found an interesting message! + break + # Returning this message + return message + +# Searching feature, to search with a list of keywords def search_redis(keywords): + # List of courses found matching with keywords courses = [] + + # For every keyword in the list of keywords separating with spaces for keyword in keywords.split(' '): if not keyword: # When the users end his keywords with a ' ' continue + + # Setting up queries for title, summary, level and teacher id (good luck for this one, searching the exact person id is not an easy task) title_query = f"@title:{keyword}" summary_query = f"@summary:{keyword}" level_query = f"@level:{keyword}" teacher_query = f"@teacher:{keyword}" + + # Searching into the indexes of the course title_results = redis_connection.execute_command('FT.SEARCH', 'idx:courses', title_query) summary_results = redis_connection.execute_command('FT.SEARCH', 'idx:courses', summary_query) level_results = redis_connection.execute_command('FT.SEARCH', 'idx:courses', level_query) teacher_results = redis_connection.execute_command('FT.SEARCH', 'idx:courses', teacher_query) + + # The redis Stack search returns as the first element the number of elements, then returns the course every 2 lines. + # I think the best way to understand it is by trying it. for i in range(1, len(title_results), 2): courses.append(title_results[i]) for i in range(1, len(summary_results), 2): @@ -261,14 +432,7 @@ def search_redis(keywords): courses.append(level_results[i]) for i in range(1, len(teacher_results), 2): courses.append(teacher_results[i]) - return courses -def notifications_redis(*courses_id): - pub_sub.punsubscribe() - pub_sub.psubscribe(*courses_id) - - for message in pub_sub.listen(): - if message["type"] != 'pmessage': - continue - break - return message \ No newline at end of file + # Returning a list with unique values (not returning 2 times the same one) + courses_set = set(courses) + return list(courses_set) \ No newline at end of file diff --git a/redis_app/views.py b/redis_app/views.py index bad4e40..a9308a0 100644 --- a/redis_app/views.py +++ b/redis_app/views.py @@ -259,7 +259,7 @@ def notifications(request): courses_id = [course['id'] for course in getCoursesFromPerson(person_id.split(":")[1])] if not courses_id: return courses(request, "You're not registered to any course. Please register to one to follow notifications.") - message = notifications_redis(*courses_id) + message = get_message(*courses_id) messages.append(message) messages.reverse() request.session["message"] = messages