You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

438 lines
18 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

# 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)