# 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) # 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): # 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 # 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, "level": course_level, "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 # 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 #################################################################################### # - 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): # 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): courses.append(summary_results[i]) for i in range(1, len(level_results), 2): courses.append(level_results[i]) for i in range(1, len(teacher_results), 2): courses.append(teacher_results[i]) # Returning a list with unique values (not returning 2 times the same one) courses_set = set(courses) return list(courses_set)