|
|
# 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 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 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 = []
|
|
|
|
|
|
# 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) |