From 7db51e204513a02b9fe3c8ae8fed188072808c1a Mon Sep 17 00:00:00 2001 From: Corentin Date: Sun, 27 Oct 2024 22:51:32 +0100 Subject: [PATCH] Added pub/sub with views :tada: --- README.md | 39 ++++++- redis_app/templates/courses.html | 1 + redis_app/templates/create.html | 1 + redis_app/templates/details.html | 1 + redis_app/templates/home.html | 4 + redis_app/templates/login.html | 1 + redis_app/templates/notifications.html | 28 +++++ redis_app/templates/profile.html | 1 + redis_app/templates/publish_message.html | 39 +++++++ redis_app/templates/register.html | 1 + redis_app/templates/search.html | 1 + redis_app/templates/update.html | 1 + redis_app/urls.py | 5 + redis_app/views.py | 138 ++++++++++++++--------- 14 files changed, 203 insertions(+), 58 deletions(-) create mode 100644 redis_app/templates/notifications.html create mode 100644 redis_app/templates/publish_message.html diff --git a/README.md b/README.md index 9681762..24cb780 100644 --- a/README.md +++ b/README.md @@ -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. -## 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 \ No newline at end of file diff --git a/redis_app/templates/courses.html b/redis_app/templates/courses.html index 634fae2..e5e7102 100644 --- a/redis_app/templates/courses.html +++ b/redis_app/templates/courses.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/create.html b/redis_app/templates/create.html index 5a31de5..ef7cbe2 100644 --- a/redis_app/templates/create.html +++ b/redis_app/templates/create.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/details.html b/redis_app/templates/details.html index bb0a085..bb5f3ca 100644 --- a/redis_app/templates/details.html +++ b/redis_app/templates/details.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/home.html b/redis_app/templates/home.html index de77586..f4c2d81 100644 --- a/redis_app/templates/home.html +++ b/redis_app/templates/home.html @@ -9,6 +9,10 @@ Courses Profile Search + Notifications + {% if person.role == 'Teacher' %} + Publish a message + {% endif %} Logout {% endif %} diff --git a/redis_app/templates/login.html b/redis_app/templates/login.html index 79e7c3c..0bd4d7c 100644 --- a/redis_app/templates/login.html +++ b/redis_app/templates/login.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/notifications.html b/redis_app/templates/notifications.html new file mode 100644 index 0000000..6c2d9f1 --- /dev/null +++ b/redis_app/templates/notifications.html @@ -0,0 +1,28 @@ + + + +

Notifications

+ {% for message in messages %} + {{ message.data }}
+ {% endfor %} + Clear notifications + + + \ No newline at end of file diff --git a/redis_app/templates/profile.html b/redis_app/templates/profile.html index 8337239..e34622b 100644 --- a/redis_app/templates/profile.html +++ b/redis_app/templates/profile.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/publish_message.html b/redis_app/templates/publish_message.html new file mode 100644 index 0000000..603382e --- /dev/null +++ b/redis_app/templates/publish_message.html @@ -0,0 +1,39 @@ + + + +
+ {% csrf_token %} +
+

Publish a message

+ + {% if error_message %} + {{ error_message }} + {% endif %} + +
+
+ +
+
+
+ + +
+ + diff --git a/redis_app/templates/register.html b/redis_app/templates/register.html index 40664b2..0709d23 100644 --- a/redis_app/templates/register.html +++ b/redis_app/templates/register.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/search.html b/redis_app/templates/search.html index 126233c..d25a4be 100644 --- a/redis_app/templates/search.html +++ b/redis_app/templates/search.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/templates/update.html b/redis_app/templates/update.html index 72c6e1c..61f9359 100644 --- a/redis_app/templates/update.html +++ b/redis_app/templates/update.html @@ -9,6 +9,7 @@ Courses Profile Search + Notifications Logout {% endif %} diff --git a/redis_app/urls.py b/redis_app/urls.py index 4ac9392..07f2008 100644 --- a/redis_app/urls.py +++ b/redis_app/urls.py @@ -15,6 +15,11 @@ urlpatterns = [ path('create_course_form', views.create_course_form, name='create_course_form'), path('search', views.search_view, name='search'), 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/', views.course_register_view, name='course_register'), path('course_unregister/', views.course_unregister_view, name='course_unregister'), path('update_course/', views.update_course_view, name='update_course'), diff --git a/redis_app/views.py b/redis_app/views.py index 4ff61c0..1c501e1 100644 --- a/redis_app/views.py +++ b/redis_app/views.py @@ -8,14 +8,36 @@ from .models import Person DEFAULT_EXPIRE_TIME = 120 EXPIRE_REFRESH_TIME = 60 -COURSE_CHANNEL = "course" -cancel = True 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) pub_sub = redis_connection.pubsub() 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): global redis_connection ttl = redis_connection.ttl(f"course:{course_id}") @@ -58,9 +80,7 @@ def getCourseId(course): def home(request): - global cancel global personLoggedIn - cancel = True return render(request, 'home.html', {'person': personLoggedIn}) def isPersonRegisteredToCourse(course): @@ -91,9 +111,7 @@ def isPersonRegisteredToCourse(course): def details(request, course_id, error_message=''): global redis_connection - global cancel global personLoggedIn - cancel = True if isExpired(course_id): raise Http404("Course has expired... Get faster next time!") course = redis_connection.hgetall(f"course:{course_id}") @@ -111,8 +129,6 @@ def details(request, course_id, error_message=''): def register(request): global personLoggedIn global redis_connection - global cancel - cancel = True if request.method == 'POST': person_name = request.POST['name'] person_role = request.POST['role'] @@ -134,8 +150,6 @@ def register(request): def login(request): global personLoggedIn global redis_connection - global cancel - cancel = True if request.method == 'POST': person_name = request.POST['name'] person_role = request.POST['role'] @@ -151,55 +165,27 @@ def login(request): return login_form(request) def register_form(request): - global cancel - cancel = True return render(request, 'register.html', {'person': Person(name="", role="")}) def login_form(request): - global cancel - cancel = True return render(request, 'login.html', {'person': Person(name="", role="")}) def logout(request): global personLoggedIn - global cancel - cancel = True personLoggedIn = "" return render(request, 'login.html', {'person': Person(name="", role="")}) def courses(request): global personLoggedIn - global cancel - cancel = True if personLoggedIn == "": return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) person_id = getPersonId(personLoggedIn["name"], personLoggedIn["role"]).split(":")[1] - 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) + courses = getCoursesFromPerson(person_id) return render(request, 'courses.html', {'person': personLoggedIn, 'courses': courses}) def change_profile(request): global personLoggedIn - global cancel - cancel = True if personLoggedIn == '': 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): global personLoggedIn - global cancel - cancel = True if personLoggedIn == '': return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) 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 }) 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): global personLoggedIn @@ -288,8 +300,6 @@ def update_course_view(request, course_id): def create_course_form(request): global personLoggedIn global redis_connection - global cancel - cancel = True if personLoggedIn == '': return render(request, 'login.html', {'person': Person(name="", role=""), "error_message": "You must login!"}) if request.method == 'POST': @@ -306,8 +316,6 @@ def create_course_form(request): def update_course_form(request, course_id): global personLoggedIn global redis_connection - global cancel - cancel = True if isExpired(course_id): raise Http404("Course has expired... Get faster next time!") if personLoggedIn == '': @@ -422,18 +430,15 @@ def refresh_expire(course_id): redis_connection.expire(f"course:{course_id}", ttl + EXPIRE_REFRESH_TIME) return True -def publish(course): - pub_sub.publish(COURSE_CHANNEL + ":" + getCourseId(course), str(course)) +def publish(course_id, message): + 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): - global cancel - cancel = False pub_sub.psubscribe(*courses_id) for message in pub_sub.listen(): - if cancel: - break if message["type"] != 'pmessage': continue send_new_course_notification(message) @@ -471,8 +476,6 @@ def search_view(request): def search_form(request): global personLoggedIn global redis_connection - global cancel - cancel = True if request.method == 'POST': keywords = request.POST['keywords'] courses = [] @@ -486,5 +489,32 @@ def search_form(request): return render(request, 'search.html', {'person': personLoggedIn, 'courses': courses}) 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 \ No newline at end of file