Added pub/sub with views 🎉

master
Corentin LEMAIRE 6 months ago
parent bb1b1ff9fc
commit 7db51e2045

@ -40,10 +40,41 @@ Now, you can go into http://localhost:8000/redis to access the project.
Don't try to go into http://localhost:8000/admin, you won't have the rights and it's only used for development mode. This helps us to add/remove data to test quicker. Don't try to go into http://localhost:8000/admin, you won't have the rights and it's only used for development mode. This helps us to add/remove data to test quicker.
## Features Once a user is registered to a course, he's automatically subscribed to courses update for all courses he follows into the notifications page.
## TODO ## Limitations
- Publish a message This is a one-user app... You can't log in with 2 users.
To test pub/sub, please follow these instructions:
- `docker start redis-stack`
- `docker exec -it redis-stack bash`
- `redis-cli`
- `psubscribe {id}` where the id is the course id where you will post messages from (with an update of the course or with Publish message). you can retrieve it with `KEYS course:*`
- Update the course or post the message from this course id
OR
- `docker start redis-stack`
- `docker exec -it redis-stack bash`
- `redis-cli`
- Go to your notifications after registered to a course (if you didn't you will get redirected to the courses page, where you can see that you don't follow any course)
- `publish {id} {message}` where id is one of the course id followed by the current user, and message is your message
---
Expiration of the course: The course expires after 2 minutes.
---
Design is not existing yet.
- Subscribe to course channel

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -9,6 +9,10 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
{% if person.role == 'Teacher' %}
<a href="{% url 'publish_message' %}">Publish a message</a>
{% endif %}
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -0,0 +1,28 @@
<html>
<body>
<nav>
<a href="{% url 'home' %}">Home</a>
{% if person.name == '' %}
<a href="{% url 'login_form' %}">Login</a>
<a href="{% url 'register_form' %}">Register</a>
{% else %}
<a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a>
{% endif %}
</nav>
<h1>Notifications</h1>
{% for message in messages %}
<a href="{% url 'details' message.channel %}">{{ message.data }}</a><br/>
{% endfor %}
<a href="{% url 'clear_notifications' %}">Clear notifications</a>
<script>
setInterval(function() {
window.location.reload();
}, 10000); // Reloads the page after 10s to avoid timeout
window.location.href = "{% url 'notifications' %}"; // Redirecting to listen to pub/sub
</script>
</body>
</html>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -0,0 +1,39 @@
<html>
<body>
<nav>
<a href="{% url 'home' %}">Home</a>
{% if person == '' %}
<a href="{% url 'login_form' %}">Login</a>
<a href="{% url 'register_form' %}">Register</a>
{% else %}
<a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a>
{% endif %}
</nav>
<form action="{% url 'publish_message_form' %}" method="POST">
{% csrf_token %}
<fieldset>
<legend><h1>Publish a message</h1></legend>
{% if error_message %}
{{ error_message }}
{% endif %}
<label for="course_id">Course</label><br>
<select name="course_id" id="course_id" required>
{% for course in courses %}
<option value="{{ course.id }}">{{ course.title }}</option>
{% endfor %}
</select><br/>
<label for="message">Message</label><br>
<input name="message" id="message" required><br/>
</fieldset>
<input type="submit" value="Publish the message">
</form>
</body>
</html>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -9,6 +9,7 @@
<a href="{% url 'courses' %}">Courses</a> <a href="{% url 'courses' %}">Courses</a>
<a href="{% url 'profile' %}">Profile</a> <a href="{% url 'profile' %}">Profile</a>
<a href="{% url 'search' %}">Search</a> <a href="{% url 'search' %}">Search</a>
<a href="{% url 'notifications_view' %}">Notifications</a>
<a href="{% url 'logout' %}">Logout</a> <a href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}
</nav> </nav>

@ -15,6 +15,11 @@ urlpatterns = [
path('create_course_form', views.create_course_form, name='create_course_form'), path('create_course_form', views.create_course_form, name='create_course_form'),
path('search', views.search_view, name='search'), path('search', views.search_view, name='search'),
path('search_form', views.search_form, name='search_form'), path('search_form', views.search_form, name='search_form'),
path('notifications_view', views.notifications_view, name='notifications_view'),
path('notifications', views.notifications, name='notifications'),
path('clear_notifications', views.clear_notifications, name='clear_notifications'),
path('publish_message', views.publish_message, name='publish_message'),
path('publish_message_form', views.publish_message_form, name='publish_message_form'),
path('course_register/<str:course_id>', views.course_register_view, name='course_register'), path('course_register/<str:course_id>', views.course_register_view, name='course_register'),
path('course_unregister/<str:course_id>', views.course_unregister_view, name='course_unregister'), path('course_unregister/<str:course_id>', views.course_unregister_view, name='course_unregister'),
path('update_course/<str:course_id>', views.update_course_view, name='update_course'), path('update_course/<str:course_id>', views.update_course_view, name='update_course'),

@ -8,14 +8,36 @@ from .models import Person
DEFAULT_EXPIRE_TIME = 120 DEFAULT_EXPIRE_TIME = 120
EXPIRE_REFRESH_TIME = 60 EXPIRE_REFRESH_TIME = 60
COURSE_CHANNEL = "course"
cancel = True
personLoggedIn = "" personLoggedIn = ""
messages = [] # All messages obtained with pub/sub. Will refresh the view every time a message is received
redis_connection = redis.Redis(host='localhost', port=6379, decode_responses=True) redis_connection = redis.Redis(host='localhost', port=6379, decode_responses=True)
pub_sub = redis_connection.pubsub() pub_sub = redis_connection.pubsub()
create_index(redis_connection=redis_connection) create_index(redis_connection=redis_connection)
def getCoursesFromPerson(person_id):
global redis_connection
course_keys = redis_connection.keys(f"course:*")
courses = []
for key in course_keys:
course_data = redis_connection.hgetall(key)
if personLoggedIn['role'] == "Student":
if person_id in course_data.get('students', '').split(","):
course_id = getCourseId2(course_data['title'], course_data['teacher'])
if course_id == False:
continue
course_data['id'] = course_id.split(":")[1]
courses.append(course_data)
else:
teacher_id = course_data["teacher"]
if teacher_id == person_id:
course_id = getCourseId2(course_data['title'], course_data['teacher'])
if course_id == False:
continue
course_data['id'] = course_id.split(":")[1]
courses.append(course_data)
return courses
def isExpired(course_id): def isExpired(course_id):
global redis_connection global redis_connection
ttl = redis_connection.ttl(f"course:{course_id}") ttl = redis_connection.ttl(f"course:{course_id}")
@ -58,9 +80,7 @@ def getCourseId(course):
def home(request): def home(request):
global cancel
global personLoggedIn global personLoggedIn
cancel = True
return render(request, 'home.html', {'person': personLoggedIn}) return render(request, 'home.html', {'person': personLoggedIn})
def isPersonRegisteredToCourse(course): def isPersonRegisteredToCourse(course):
@ -91,9 +111,7 @@ def isPersonRegisteredToCourse(course):
def details(request, course_id, error_message=''): def details(request, course_id, error_message=''):
global redis_connection global redis_connection
global cancel
global personLoggedIn global personLoggedIn
cancel = True
if isExpired(course_id): if isExpired(course_id):
raise Http404("Course has expired... Get faster next time!") raise Http404("Course has expired... Get faster next time!")
course = redis_connection.hgetall(f"course:{course_id}") course = redis_connection.hgetall(f"course:{course_id}")
@ -111,8 +129,6 @@ def details(request, course_id, error_message=''):
def register(request): def register(request):
global personLoggedIn global personLoggedIn
global redis_connection global redis_connection
global cancel
cancel = True
if request.method == 'POST': if request.method == 'POST':
person_name = request.POST['name'] person_name = request.POST['name']
person_role = request.POST['role'] person_role = request.POST['role']
@ -134,8 +150,6 @@ def register(request):
def login(request): def login(request):
global personLoggedIn global personLoggedIn
global redis_connection global redis_connection
global cancel
cancel = True
if request.method == 'POST': if request.method == 'POST':
person_name = request.POST['name'] person_name = request.POST['name']
person_role = request.POST['role'] person_role = request.POST['role']
@ -151,55 +165,27 @@ def login(request):
return login_form(request) return login_form(request)
def register_form(request): def register_form(request):
global cancel
cancel = True
return render(request, 'register.html', {'person': Person(name="", role="")}) return render(request, 'register.html', {'person': Person(name="", role="")})
def login_form(request): def login_form(request):
global cancel
cancel = True
return render(request, 'login.html', {'person': Person(name="", role="")}) return render(request, 'login.html', {'person': Person(name="", role="")})
def logout(request): def logout(request):
global personLoggedIn global personLoggedIn
global cancel
cancel = True
personLoggedIn = "" personLoggedIn = ""
return render(request, 'login.html', {'person': Person(name="", role="")}) return render(request, 'login.html', {'person': Person(name="", role="")})
def courses(request): def courses(request):
global personLoggedIn global personLoggedIn
global cancel
cancel = True
if personLoggedIn == "": if personLoggedIn == "":
return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"})
person_id = getPersonId(personLoggedIn["name"], personLoggedIn["role"]).split(":")[1] person_id = getPersonId(personLoggedIn["name"], personLoggedIn["role"]).split(":")[1]
course_keys = redis_connection.keys(f"course:*") courses = getCoursesFromPerson(person_id)
courses = []
for key in course_keys:
course_data = redis_connection.hgetall(key)
if personLoggedIn['role'] == "Student":
if person_id in course_data.get('students', '').split(","):
course_id = getCourseId2(course_data['title'], course_data['teacher'])
if course_id == False:
continue
course_data['id'] = course_id.split(":")[1]
courses.append(course_data)
else:
teacher_id = course_data["teacher"]
if teacher_id == person_id:
course_id = getCourseId2(course_data['title'], course_data['teacher'])
if course_id == False:
continue
course_data['id'] = course_id.split(":")[1]
courses.append(course_data)
return render(request, 'courses.html', {'person': personLoggedIn, 'courses': courses}) return render(request, 'courses.html', {'person': personLoggedIn, 'courses': courses})
def change_profile(request): def change_profile(request):
global personLoggedIn global personLoggedIn
global cancel
cancel = True
if personLoggedIn == '': if personLoggedIn == '':
return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"})
@ -220,8 +206,6 @@ def change_profile(request):
def profile(request): def profile(request):
global personLoggedIn global personLoggedIn
global cancel
cancel = True
if personLoggedIn == '': if personLoggedIn == '':
return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"})
return render(request, 'profile.html', {'person': personLoggedIn}) return render(request, 'profile.html', {'person': personLoggedIn})
@ -267,6 +251,34 @@ def update_course(course_id, course_title, course_summary, course_level, course_
"teacher": course_teacher "teacher": course_teacher
}) })
redis_connection.expire(f"course:{course_id}", DEFAULT_EXPIRE_TIME) redis_connection.expire(f"course:{course_id}", DEFAULT_EXPIRE_TIME)
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}")
def publish_message(request):
global personLoggedIn
courses_fetched = getCoursesFromPerson(getPersonId(personLoggedIn['name'], personLoggedIn['role']).split(":")[1])
for course in courses_fetched:
course['id'] = getCourseId(course).split(':')[1]
if not courses_fetched:
return courses(request)
return render(request, 'publish_message.html', {'person': personLoggedIn, 'courses': courses_fetched})
def publish_message_form(request):
global personLoggedIn
global redis_connection
if personLoggedIn == '':
return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"})
if personLoggedIn['role'] == 'Student':
return render(request, 'publish_message.html', {'person': personLoggedIn, "error_message": "The form has been wrongly filled!"})
if request.method == 'POST':
course_id = request.POST['course_id']
message = request.POST['message']
course = redis_connection.hgetall(f'course:{course_id}')
if not course:
raise Http404("Course not found")
publish(course_id, message)
return publish_message(request)
return render(request, 'publish_message.html', {'person': personLoggedIn, "error_message": "The form has been wrongly filled!"})
def create_course_view(request): def create_course_view(request):
global personLoggedIn global personLoggedIn
@ -288,8 +300,6 @@ def update_course_view(request, course_id):
def create_course_form(request): def create_course_form(request):
global personLoggedIn global personLoggedIn
global redis_connection global redis_connection
global cancel
cancel = True
if personLoggedIn == '': if personLoggedIn == '':
return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"})
if request.method == 'POST': if request.method == 'POST':
@ -306,8 +316,6 @@ def create_course_form(request):
def update_course_form(request, course_id): def update_course_form(request, course_id):
global personLoggedIn global personLoggedIn
global redis_connection global redis_connection
global cancel
cancel = True
if isExpired(course_id): if isExpired(course_id):
raise Http404("Course has expired... Get faster next time!") raise Http404("Course has expired... Get faster next time!")
if personLoggedIn == '': if personLoggedIn == '':
@ -422,18 +430,15 @@ def refresh_expire(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
def publish(course): def publish(course_id, message):
pub_sub.publish(COURSE_CHANNEL + ":" + getCourseId(course), str(course)) global redis_connection
redis_connection.publish(course_id, message)
# Courses is a list of coursee id to subscribe to # Courses is a list of course id to subscribe to
def subscribe(courses_id): def subscribe(courses_id):
global cancel
cancel = False
pub_sub.psubscribe(*courses_id) pub_sub.psubscribe(*courses_id)
for message in pub_sub.listen(): for message in pub_sub.listen():
if cancel:
break
if message["type"] != 'pmessage': if message["type"] != 'pmessage':
continue continue
send_new_course_notification(message) send_new_course_notification(message)
@ -471,8 +476,6 @@ def search_view(request):
def search_form(request): def search_form(request):
global personLoggedIn global personLoggedIn
global redis_connection global redis_connection
global cancel
cancel = True
if request.method == 'POST': if request.method == 'POST':
keywords = request.POST['keywords'] keywords = request.POST['keywords']
courses = [] courses = []
@ -487,4 +490,31 @@ def search_form(request):
return render(request, 'search.html', {'person': personLoggedIn, "error_message": "The form has been wrongly filled!"}) return render(request, 'search.html', {'person': personLoggedIn, "error_message": "The form has been wrongly filled!"})
def notifications_view(request):
global personLoggedIn
global messages
return render(request, 'notifications.html', {'person': personLoggedIn, 'messages': messages})
def notifications(request):
global personLoggedIn
global messages
if personLoggedIn == "":
return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"})
pub_sub.punsubscribe()
courses_id = [course['id'] for course in getCoursesFromPerson(getPersonId(personLoggedIn['name'], personLoggedIn['role']).split(":")[1])]
if not courses_id:
return courses(request)
pub_sub.psubscribe(*courses_id)
for message in pub_sub.listen():
if message["type"] != 'pmessage':
continue
messages.append(message)
return notifications_view(request)
def clear_notifications(request):
global messages
messages = []
return notifications_view(request)
# Every id is only the number, not the course:number or person:number # Every id is only the number, not the course:number or person:number
Loading…
Cancel
Save