@ -1,118 +1,172 @@
# To use redis
import redis
import redis
# To generate some Id for course and persons
import uuid
import uuid
# To create the index to setup the search feature
from redis_app . utils . index import create_index
from redis_app . utils . index import create_index
# Constants
# Will expire after 300s = 5 mins
DEFAULT_EXPIRE_TIME = 300
DEFAULT_EXPIRE_TIME = 300
# The refresh add 60s = 1 min
EXPIRE_REFRESH_TIME = 60
EXPIRE_REFRESH_TIME = 60
# Setting up the redis connection to localhost!6379
redis_connection = redis . Redis ( host = ' localhost ' , port = 6379 , decode_responses = True )
redis_connection = redis . Redis ( host = ' localhost ' , port = 6379 , decode_responses = True )
# Setting up the redis pubsub system
pub_sub = redis_connection . pubsub ( )
pub_sub = redis_connection . pubsub ( )
# Creating the index for the search feature
create_index ( redis_connection )
create_index ( redis_connection )
####################################################################################
####################################################################################
# - PERSONS - #
# - 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 ) :
def getPersonIdNameRole ( name , role ) :
# Defining the query for Redis Stack
query = f " @name: { name } @role: { role } "
query = f " @name: { name } @role: { role } "
# Searching with redis Stack
results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:persons ' , query )
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 :
if results [ 0 ] > 0 :
# Taking the first result
person_key = results [ 1 ]
person_key = results [ 1 ]
return person_key
return person_key
# No person found
return False
return False
# Gets the person id with the person object (which obviously doesn't contain the id)
def getPersonId ( person ) :
def getPersonId ( person ) :
return getPersonIdNameRole ( person [ " name " ] , person [ " role " ] )
return getPersonIdNameRole ( person [ " name " ] , person [ " role " ] )
# Gets the whole person object with the person id
def getPerson ( person_id ) :
def getPerson ( person_id ) :
return redis_connection . hgetall ( f " person: { person_id } " )
return redis_connection . hgetall ( f " person: { person_id } " )
# Allows a user to register
def register_redis ( name , role ) :
def register_redis ( name , role ) :
# Defining the query for Redis Stack
query = f " @name: { name } @role: { role } "
query = f " @name: { name } @role: { role } "
# Searching with redis Stack
results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:persons ' , query )
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 :
if results [ 0 ] > 0 :
# The user already exists.
return False
return False
# Creating the person_key
person_key = f " person: { get_uuid4 ( ) } "
person_key = f " person: { get_uuid4 ( ) } "
# And assigning correct information
redis_connection . hset ( person_key , mapping = {
redis_connection . hset ( person_key , mapping = {
' name ' : name ,
' name ' : name ,
' role ' : role
' role ' : role
} )
} )
# Retrieving the user just created (we're sure it's been well created with this approach)
return redis_connection . hgetall ( person_key )
return redis_connection . hgetall ( person_key )
# Allows a user to login
def login_redis ( name , role ) :
def login_redis ( name , role ) :
query = f " @name: { name } @role: { role } "
# We just check if the user exists
results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:persons ' , query )
return getPersonIdNameRole ( name , role )
# First element = number of results
if results [ 0 ] > 0 :
person_key = results [ 1 ]
return redis_connection . hgetall ( person_key )
return False
# 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 ) :
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 )
new_person_id = getPersonIdNameRole ( name , role )
if new_person_id :
if new_person_id :
return False
return False
# Setting the new information
redis_connection . hmset ( person_id , mapping = { " name " : name , " role " : role } )
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 ] )
return getPerson ( person_id . split ( ' : ' ) [ 1 ] )
####################################################################################
####################################################################################
# - COURSES - #
# - COURSES - #
####################################################################################
####################################################################################
# Is a course expired?
def isExpired ( course_id ) :
def isExpired ( course_id ) :
# Checking the time to live of a course
ttl = redis_connection . ttl ( f " course: { course_id } " )
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
if ttl == - 2 or ttl == 0 : # If -1, the key has no expiration
return True
return True
return False
return False
# Is the course full? Is there a place for a new student?
def isCourseFull ( course ) :
def isCourseFull ( course ) :
students = course . get ( ' students ' , ' ' ) # Because it crashes with ['students'], we have to put a default value: '' here
students = course . get ( ' students ' , ' ' ) # Because it crashes with ['students'], we have to put a default value: '' here
if not students :
if not students :
# No one is registered, and since the minimum of place for a course is 1, it's not full
return False
return False
try :
try :
# We want an int to compare
places = int ( course [ ' places ' ] )
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 :
if len ( students . split ( ' , ' ) ) > = places :
return True
return True
return False
return False
except :
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
return False
# Gets the id of the whole course object (obviously without the id in it)
def getCourseId ( course ) :
def getCourseId ( course ) :
# Setting up the query to search with redis Stack
query = f " @title: { course [ ' title ' ] } @teacher: { course [ ' teacher ' ] } "
query = f " @title: { course [ ' title ' ] } @teacher: { course [ ' teacher ' ] } "
# Searching with Redis Stack
results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:courses ' , query )
results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:courses ' , query )
# If the number of results > 0
if results [ 0 ] > 0 :
if results [ 0 ] > 0 :
# Returning the first course
course_key = results [ 1 ]
course_key = results [ 1 ]
return course_key
return course_key
# No course found
return False
return False
# Gets the whole course object with the course_id
def getCourse ( course_id ) :
def getCourse ( course_id ) :
return redis_connection . hgetall ( f " course: { course_id } " )
return redis_connection . hgetall ( f " course: { course_id } " )
# Deleting a course
def delete_course ( course_id ) :
def delete_course ( course_id ) :
# Checking if the course exists
course = redis_connection . hgetall ( f " course: { course_id } " )
course = redis_connection . hgetall ( f " course: { course_id } " )
if not course :
if not course :
# Does not exist
return False
return False
# Deleting it
redis_connection . delete ( f " course: { course_id } " )
redis_connection . delete ( f " course: { course_id } " )
return True
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 ) :
def create_course ( course_title , course_summary , course_level , course_places , course_teacher ) :
# Setting up the course id
course_id = get_uuid4 ( )
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 = {
redis_connection . hset ( f " course: { course_id } " , mapping = {
" title " : course_title ,
" title " : course_title ,
" summary " : course_summary ,
" summary " : course_summary ,
" level " : course_level ,
" level " : course_level ,
" places " : course_places ,
" 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 )
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 ) :
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 = {
redis_connection . hset ( f " course: { course_id } " , mapping = {
" title " : course_title ,
" title " : course_title ,
" summary " : course_summary ,
" summary " : course_summary ,
@ -120,62 +174,90 @@ def update_course(course_id, course_title, course_summary, course_level, course_
" places " : course_places ,
" places " : course_places ,
" teacher " : course_teacher
" teacher " : course_teacher
} )
} )
# Expire after DEFAULT_EXPIRE_TIME seconds
redis_connection . expire ( f " course: { course_id } " , DEFAULT_EXPIRE_TIME )
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 } " )
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 ) :
def course_register ( course_id , person_id ) :
# Checking if the course and the person exists
course = redis_connection . hgetall ( f " course: { course_id } " )
course = redis_connection . hgetall ( f " course: { course_id } " )
person = redis_connection . hgetall ( f " person: { person_id } " )
person = redis_connection . hgetall ( f " person: { person_id } " )
if not course or not person :
if not course or not person :
# One or both don't exist
return False
return False
# Taking the students of the course
students = course . get ( ' students ' , ' ' )
students = course . get ( ' students ' , ' ' )
# Checking if the user is not already registered
if person_id in students . split ( ' , ' ) :
if person_id in students . split ( ' , ' ) :
# He's registered
return True
return True
try :
# Checking if the course is full
places = int ( course [ ' places ' ] )
if isCourseFull ( course ) :
if students != ' ' :
students_nb = len ( students . split ( ' , ' ) )
if students_nb and students_nb > = places :
return False
except :
return False
return False
# if there's not student registered to the course, adding the person id to it
if not students :
if not students :
new_students = person_id
new_students = person_id
else :
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
new_students = students + " , " + person_id
# Setting the new students list
redis_connection . hset ( f " course: { course_id } " , " students " , new_students )
redis_connection . hset ( f " course: { course_id } " , " students " , new_students )
refresh_expire ( course_id )
return True
return True
def course_unregister ( course_id , person_id ) :
def course_unregister ( course_id , person_id ) :
# Checking if the course and the person exists
course = redis_connection . hgetall ( f " course: { course_id } " )
course = redis_connection . hgetall ( f " course: { course_id } " )
person = redis_connection . hgetall ( f " person: { person_id } " )
person = redis_connection . hgetall ( f " person: { person_id } " )
if not course or not person :
if not course or not person :
# One or both don't exist
return False
return False
# Taking the students of the course
students = course . get ( ' students ' , ' ' )
students = course . get ( ' students ' , ' ' )
# Checking if the user is registered
if person_id not in students . split ( ' , ' ) :
if person_id not in students . split ( ' , ' ) :
# Not registered, so it's all good
return True
return True
# Creating a list of all students by spliting it by the coma
students_list = students . split ( " , " )
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 :
if len ( students_list ) == 1 :
# The new list is empty
new_students = " "
new_students = " "
else :
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 ] )
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 )
redis_connection . hset ( f " course: { course_id } " , " students " , new_students )
return True
return True
# Allows to refresh the course ttl when someone register to the course
def refresh_expire ( course_id ) :
def refresh_expire ( course_id ) :
# Checking if the course exists
course = redis_connection . hgetall ( f " course: { course_id } " )
course = redis_connection . hgetall ( f " course: { course_id } " )
if not course :
if not course :
# Does not exist
return False
return False
# Checking the ttl
ttl = redis_connection . ttl ( f " course: { course_id } " )
ttl = redis_connection . ttl ( f " course: { course_id } " )
if ttl < = 0 :
if ttl < = 0 :
# ttl is < 0 so it's expired, or does not exist at all, or has an infinite ttl
return False
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 )
redis_connection . expire ( f " course: { course_id } " , ttl + EXPIRE_REFRESH_TIME )
return True
return True
@ -183,76 +265,165 @@ def refresh_expire(course_id):
# - LINKS - #
# - LINKS - #
####################################################################################
####################################################################################
# Gets the courses of a person by its id
def getCoursesFromPerson ( person_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:* " )
course_keys = redis_connection . keys ( f " course:* " )
courses = [ ]
courses = [ ]
# Checking if the person exists
person = redis_connection . hgetall ( f " person: { person_id } " )
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 :
for key in course_keys :
# Getting the data of the course key
course_data = redis_connection . hgetall ( key )
course_data = redis_connection . hgetall ( key )
# When the user is a student
if person [ ' role ' ] == " Student " :
if person [ ' role ' ] == " Student " :
# We're only interested about courses where the student is
if person_id in course_data . get ( ' students ' , ' ' ) . split ( " , " ) :
if person_id in course_data . get ( ' students ' , ' ' ) . split ( " , " ) :
# Getting the course id
course_id = getCourseId ( course_data )
course_id = getCourseId ( course_data )
if course_id == False :
if course_id == False :
# Didn't work, continuing not to break the feature
continue
continue
# Adding the id manually the course object
course_data [ ' id ' ] = course_id . split ( " : " ) [ 1 ]
course_data [ ' id ' ] = course_id . split ( " : " ) [ 1 ]
# Adding the course object to the courses list we will return
courses . append ( course_data )
courses . append ( course_data )
# When the user is a teacher
else :
else :
# taking the teacher id of the courses
teacher_id = course_data [ " teacher " ]
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 :
if teacher_id == person_id :
# Getting the course idc
course_id = getCourseId ( course_data )
course_id = getCourseId ( course_data )
if course_id == False :
if course_id == False :
# Didn't work, continuing not to break the feature
continue
continue
# Adding the id manually the course object
course_data [ ' id ' ] = course_id . split ( " : " ) [ 1 ]
course_data [ ' id ' ] = course_id . split ( " : " ) [ 1 ]
# Adding the course object to the courses list we will return
courses . append ( course_data )
courses . append ( course_data )
# Returning the courses found
return courses
return courses
# Is the person registered to the course?
def isPersonRegisteredToCourse ( course , person ) :
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
return False
# Getting the person id
person_id = getPersonId ( person ) . split ( " : " ) [ 1 ]
person_id = getPersonId ( person ) . split ( " : " ) [ 1 ]
if not person_id :
if not person_id :
# Not found
return False
return False
# Getting the course id
course_id = getCourseId ( course )
course_id = getCourseId ( course )
if not course_id :
if not course_id :
# Not found
return False
return False
# Iterating over each course keys in Redis
course_keys = redis_connection . keys ( f " course:* " )
course_keys = redis_connection . keys ( f " course:* " )
for key in course_keys :
for key in course_keys :
# Getting the course data
course_data = redis_connection . hgetall ( key )
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 " :
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 ( " , " ) :
if person_id in course_data . get ( ' students ' , ' ' ) . split ( " , " ) :
# Getting the course id of the current course
current_course_id = getCourseId ( course_data )
current_course_id = getCourseId ( course_data )
if current_course_id == False :
if current_course_id == False :
# Didn't work, continuing not to break the feature
continue
continue
# Checking if the current course id equals the course id requested
if course_id == current_course_id :
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
return True
# After iterating over all keys, the students is not into the course_id
return False
return False
####################################################################################
####################################################################################
# - UTILS - #
# - UTILS - #
####################################################################################
####################################################################################
# Creating an uuid
def get_uuid4 ( ) :
def get_uuid4 ( ) :
# We remove the - because it creates a lot of problems into redis for the search feature
return str ( uuid . uuid4 ( ) ) . replace ( ' - ' , ' ' )
return str ( uuid . uuid4 ( ) ) . replace ( ' - ' , ' ' )
# Publish a message for a course
def publish ( course_id , message ) :
def publish ( course_id , message ) :
# Publishing the message "message" into the channel course_id
redis_connection . publish ( course_id , message )
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 ) :
def search_redis ( keywords ) :
# List of courses found matching with keywords
courses = [ ]
courses = [ ]
# For every keyword in the list of keywords separating with spaces
for keyword in keywords . split ( ' ' ) :
for keyword in keywords . split ( ' ' ) :
if not keyword : # When the users end his keywords with a ' '
if not keyword : # When the users end his keywords with a ' '
continue
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 } "
title_query = f " @title: { keyword } "
summary_query = f " @summary: { keyword } "
summary_query = f " @summary: { keyword } "
level_query = f " @level: { keyword } "
level_query = f " @level: { keyword } "
teacher_query = f " @teacher: { 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 )
title_results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:courses ' , title_query )
summary_results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:courses ' , summary_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 )
level_results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:courses ' , level_query )
teacher_results = redis_connection . execute_command ( ' FT.SEARCH ' , ' idx:courses ' , teacher_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 ) :
for i in range ( 1 , len ( title_results ) , 2 ) :
courses . append ( title_results [ i ] )
courses . append ( title_results [ i ] )
for i in range ( 1 , len ( summary_results ) , 2 ) :
for i in range ( 1 , len ( summary_results ) , 2 ) :
@ -261,14 +432,7 @@ def search_redis(keywords):
courses . append ( level_results [ i ] )
courses . append ( level_results [ i ] )
for i in range ( 1 , len ( teacher_results ) , 2 ) :
for i in range ( 1 , len ( teacher_results ) , 2 ) :
courses . append ( teacher_results [ i ] )
courses . append ( teacher_results [ i ] )
return courses
def notifications_redis ( * courses_id ) :
# Returning a list with unique values (not returning 2 times the same one)
pub_sub . punsubscribe ( )
courses_set = set ( courses )
pub_sub . psubscribe ( * courses_id )
return list ( courses_set )
for message in pub_sub . listen ( ) :
if message [ " type " ] != ' pmessage ' :
continue
break
return message