diff --git a/django_project/django_project/urls.py b/django_project/django_project/urls.py index ae506df4ca0b4fbc500750cb0b75fe57fb4d3cdc..0e0131b7b98113ac7e6c3714fdf502c56c13e4a7 100644 --- a/django_project/django_project/urls.py +++ b/django_project/django_project/urls.py @@ -20,7 +20,7 @@ from django.conf import settings from django.conf.urls.static import static from myapp.views.authviews import login_view, logout_view -from myapp.views.userviews import register_view, account_view, delete_account_view +from myapp.views.userviews import register_view, account_view, delete_account_view, welcome_view, edit_account_view from myapp.views.groupviews import group_list_view, group_create_view, group_update_view, group_delete_view, group_detail_view from myapp.views.postviews import create_post_view, feed_view from myapp.views.initview import init_db_view @@ -40,6 +40,8 @@ urlpatterns = [ path('groups/<int:group_id>/', group_detail_view, name='group_detail'), path('post/create/', create_post_view, name='create_post'), path('feed/', feed_view, name='feed'), + path('welcome/', welcome_view, name='welcome'), + path('edit_account/', edit_account_view, name='edit_account') ] if settings.DEBUG: diff --git a/django_project/myapp/.DS_Store b/django_project/myapp/.DS_Store index 674394648b1cfb241b58e57b2016a69134322d6d..39abac5ecaf4a1ffbf0aa011fdbf24cbc678bff0 100644 Binary files a/django_project/myapp/.DS_Store and b/django_project/myapp/.DS_Store differ diff --git a/django_project/myapp/templates/myapp/account.html b/django_project/myapp/templates/myapp/account.html index e741f80d43504b0a68717f3599038abd48be5b90..d8480474864f01958397c6fa660f5fb750e4374c 100644 --- a/django_project/myapp/templates/myapp/account.html +++ b/django_project/myapp/templates/myapp/account.html @@ -12,6 +12,7 @@ {% else %} <p>No profile picture</p> {% endif %} + <p><a href="{% url 'edit_account' %}">Edit Account</a></p> <p><a href="{% url 'delete_account' %}">Delete Account</a></p> <p><a href="{% url 'logout' %}">Logout</a></p> {% endblock %} diff --git a/django_project/myapp/templates/myapp/edit_account.html b/django_project/myapp/templates/myapp/edit_account.html new file mode 100644 index 0000000000000000000000000000000000000000..6d94253663279a30a643ac7814a8c456179b3aab --- /dev/null +++ b/django_project/myapp/templates/myapp/edit_account.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Edit Account</title> + <!-- You can include your CSS files or Bootstrap here --> + <style> + label { + display: block; + margin-top: 10px; + } + input, button { + margin-top: 5px; + } + .profile-pic { + max-width: 200px; + margin-top: 5px; + } + .error { + color: red; + } + </style> +</head> +<body> + <h1>Edit Your Account</h1> + + {% if error %} + <p class="error">{{ error }}</p> + {% endif %} + + <form method="POST" enctype="multipart/form-data"> + {% csrf_token %} + + <label for="name">Name:</label> + <input type="text" name="name" id="name" value="{{ user.name }}" required> + + <label for="email">Email:</label> + <input type="email" name="email" id="email" value="{{ user.email }}" required> + + <label for="username">Username:</label> + <input type="text" name="username" id="username" value="{{ user.username }}" required> + + <label for="studentid">Student ID:</label> + <input type="number" name="studentid" id="studentid" value="{{ user.studentid }}" required> + + <label for="startyear">Start Year:</label> + <input type="number" name="startyear" id="startyear" value="{{ user.startyear }}" required> + + <label for="endyear">End Year:</label> + <input type="number" name="endyear" id="endyear" value="{{ user.endyear }}" required> + + <label for="profilepicture">Profile Picture:</label> + {% if user.profilepicture %} + <img src="{{ user.profilepicture }}" alt="Profile Picture" class="profile-pic"> + {% endif %} + <input type="file" name="profilepicture" id="profilepicture"> + + <br><br> + <button type="submit">Update Account</button> + </form> +</body> +</html> + diff --git a/django_project/myapp/templates/myapp/welcome_view.html b/django_project/myapp/templates/myapp/welcome_view.html new file mode 100644 index 0000000000000000000000000000000000000000..f6bd6ad0a4d96a40161d7bcde8e29a2cd013af3d --- /dev/null +++ b/django_project/myapp/templates/myapp/welcome_view.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <title>Welcome!</title> +</head> +<body> + <h1>Welcome to Our App!</h1> + <p>Thank you for signing up.</p> + <a href="{% url 'feed' %}">Continue to your feed</a> +</body> +</html> + diff --git a/django_project/myapp/views/authviews.py b/django_project/myapp/views/authviews.py index 5ab6630e4bb8ab9939eb715e67b3e07690de27e7..93aa83762531352fd17577b94ee0a9ab697bb6cc 100644 --- a/django_project/myapp/views/authviews.py +++ b/django_project/myapp/views/authviews.py @@ -10,48 +10,61 @@ db_url = settings.DATABASE_URL def login_view(request): """ Handles user login by verifying credentials using the Rust extension. - If valid, stores user details in the session and redirects to the account page. + If valid, stores user details in the session and redirects accordingly. """ db_url = settings.DATABASE_URL context = {} - + if request.method == 'POST': email = request.POST.get('email', '').strip() password = request.POST.get('password', '').strip() - + if not email or not password: context['error'] = "Email and password are required." return render(request, 'myapp/login.html', context) - + try: - # Verify the user's credentials using the Rust extension. + # Verify credentials using Rust extension. is_valid = rust_crud_api.verify_user(db_url, email, password) if not is_valid: context['error'] = "Invalid email or password." return render(request, 'myapp/login.html', context) - - # Retrieve the user record to store additional info in session. - # (For simplicity, we retrieve all users and select the matching one.) + + # Retrieve the user record. + # (Assuming get_all_users returns objects with a firstlogin attribute.) users = rust_crud_api.get_all_users(db_url) user = next((u for u in users if u.email.lower() == email.lower()), None) if not user: context['error'] = "User not found." return render(request, 'myapp/login.html', context) - - # Store user information in the session. + + # Store user info in session. request.session['user_id'] = user.id request.session['user_name'] = user.name request.session['user_email'] = user.email - - return redirect('feed') + + # Check first login flag. + if user.firstlogin: + # Update flag to false so that this splash won't appear again. + rust_crud_api.update_first_login(db_url, user.id, False) + # Optionally, set a session flag if you want to show the welcome page only once. + request.session['first_login'] = True + return redirect('welcome') + else: + return redirect('feed') except Exception as e: context['error'] = f"An error occurred: {str(e)}" return render(request, 'myapp/login.html', context) - + return render(request, 'myapp/login.html', context) def logout_view(request): # Clear all session data to log out the user. request.session.flush() # Optionally, render a confirmation page or redirect to login. - return render(request, 'myapp/logout.html') \ No newline at end of file + return render(request, 'myapp/logout.html') + +def welcome_view(request): + if not request.session.get('first_login'): + return redirect('feed') + return render(request, 'myapp/welcome.html') \ No newline at end of file diff --git a/django_project/myapp/views/userviews.py b/django_project/myapp/views/userviews.py index 0c5bedfdbe17a58731bc2db43d349d8857a14022..539c3e562e32a8c68bf68dbffe14d1d4597f5d6e 100644 --- a/django_project/myapp/views/userviews.py +++ b/django_project/myapp/views/userviews.py @@ -81,18 +81,7 @@ def register_view(request): # 4. Call Rust to create the user try: - rust_crud_api.create_user( - db_url, - name, - email, - password, - username, - studentid, - startyear, - endyear, - saved_path, - True - ) + rust_crud_api.create_user( db_url,name, email, password, username, studentid, startyear, endyear, saved_path, True) # 5. After successful registration, redirect to login or some other page return redirect('login') except Exception as e: @@ -139,4 +128,93 @@ def delete_account_view(request): return JsonResponse({'error': 'Account deletion failed.'}, status=500) except Exception as e: return JsonResponse({'error': str(e)}, status=500) - return render(request, 'myapp/delete_account.html') \ No newline at end of file + return render(request, 'myapp/delete_account.html') + +def welcome_view(request): + return render(request, 'myapp/welcome_view.html') + +def edit_account_view(request): + db_url = settings.DATABASE_URL + context = {} + user_id = request.session.get('user_id') + + # If user is not logged in, redirect them to login + if not user_id: + return redirect('login') + + if request.method == 'POST': + # Get form values + new_name = request.POST.get('name', '').strip() + new_email = request.POST.get('email', '').strip() + new_username = request.POST.get('username', '').strip() + new_studentid_str = request.POST.get('studentid', '').strip() + new_startyear_str = request.POST.get('startyear', '').strip() + new_endyear_str = request.POST.get('endyear', '').strip() + + # Handle profile picture upload, if any. + profilepicture_file = request.FILES.get('profilepicture') + saved_path = None + if profilepicture_file: + filename = profilepicture_file.name + # Decide the folder inside MEDIA_ROOT for profile pics. + profile_dir = os.path.join(settings.MEDIA_ROOT, 'profile_pics') + os.makedirs(profile_dir, exist_ok=True) + # Full filesystem path where file will be saved. + save_path = os.path.join(profile_dir, filename) + with open(save_path, 'wb+') as destination: + for chunk in profilepicture_file.chunks(): + destination.write(chunk) + # Build the URL path to store in DB. + saved_path = f"/media/profile_pics/{filename}" + + # Convert numeric fields; if conversion fails, re-render with an error. + try: + new_studentid = int(new_studentid_str) if new_studentid_str else None + except ValueError: + context['error'] = "Student ID must be a number." + return render(request, 'myapp/edit_account.html', context) + + try: + new_startyear = int(new_startyear_str) if new_startyear_str else None + except ValueError: + context['error'] = "Start year must be a number." + return render(request, 'myapp/edit_account.html', context) + + try: + new_endyear = int(new_endyear_str) if new_endyear_str else None + except ValueError: + context['error'] = "End year must be a number." + return render(request, 'myapp/edit_account.html', context) + + # Call the flexible update_user function from your Rust API. + # Here we pass None for fields the user hasn't changed. + try: + rust_crud_api.update_user( + db_url, + int(user_id), + new_name if new_name else None, + new_email if new_email else None, + None, # We're not updating the password here. + new_username if new_username else None, + new_studentid, + new_startyear, + new_endyear, + saved_path, # We leave firstlogin unchanged on account edits. + ) + # Redirect to the account page after a successful update. + return redirect('account') + except Exception as e: + context['error'] = f"An error occurred: {str(e)}" + # Fall through to re-render the form with error message. + + # For GET (or if POST fails), fetch the current user details. + try: + user = rust_crud_api.get_user(db_url, int(user_id)) + if not user: + return redirect('login') + except Exception as e: + context['error'] = f"Error fetching user data: {str(e)}" + user = None + + context['user'] = user + return render(request, 'myapp/edit_account.html', context) diff --git a/rust_crud_api/src/db/init.rs b/rust_crud_api/src/db/init.rs index 1c45983c03cb31a15e69351b03c7ce81e15ac59f..045676d1694b20b24dae3ec5550dae545a202e05 100644 --- a/rust_crud_api/src/db/init.rs +++ b/rust_crud_api/src/db/init.rs @@ -20,8 +20,8 @@ pub fn init_db(db_url: &str) -> PyResult<()> { studentid INT NOT NULL UNIQUE, startyear INT NOT NULL, endyear INT NOT NULL, - profilepicture VARCHAR - firstlogin BOOLEAN NOT NULL DEFAULT TRUE, + profilepicture VARCHAR, + firstlogin BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE IF NOT EXISTS groups ( id SERIAL PRIMARY KEY, diff --git a/rust_crud_api/src/db/users.rs b/rust_crud_api/src/db/users.rs index ec15724ba431621aed3eabaec395ee474cde10e3..6c71f1dfeb7ae0e4e1a34496d18494ca5bf80be4 100644 --- a/rust_crud_api/src/db/users.rs +++ b/rust_crud_api/src/db/users.rs @@ -4,6 +4,7 @@ use pyo3::exceptions::PyRuntimeError; use postgres::{Client, NoTls}; use crate::auth::hash_password; use crate::auth::verify_password; +use postgres::types::ToSql; fn pg_err(e: postgres::Error) -> PyErr { PyRuntimeError::new_err(e.to_string()) @@ -69,7 +70,7 @@ pub fn get_user(db_url: &str, user_id: i32) -> PyResult<Option<User>> { #[pyfunction] pub fn get_all_users(db_url: &str) -> PyResult<Vec<User>> { let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let rows = client.query("SELECT id, name, email, username, studentid, startyear, endyear, profilepicture FROM users", &[]) + let rows = client.query("SELECT id, name, email, username, studentid, startyear, endyear, profilepicture, firstlogin FROM users", &[]) .map_err(pg_err)?; let mut users = Vec::new(); @@ -90,14 +91,74 @@ pub fn get_all_users(db_url: &str) -> PyResult<Vec<User>> { Ok(users) } -/// Update an existing user with a new name and email. -#[pyfunction] -pub fn update_user(db_url: &str, user_id: i32, new_name: &str, new_email: &str) -> PyResult<()> { +// Only include optional parameters in the `signature` attribute +#[pyfunction(signature = ( + db_url, + user_id, + new_name = None, + new_email = None, + new_password = None, + new_username = None, + new_studentid = None, + new_startyear = None, + new_endyear = None, + new_profilepicture = None +))] +pub fn update_user( + db_url: &str, // Required, do NOT include in signature + user_id: i32, // Required, do NOT include in signature + new_name: Option<&str>, + new_email: Option<&str>, + new_password: Option<&str>, + new_username: Option<&str>, + new_studentid: Option<i32>, + new_startyear: Option<i32>, + new_endyear: Option<i32>, + new_profilepicture: Option<&str>, +) -> PyResult<()> { let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; - let updated = client.execute( - "UPDATE users SET name = $1, email = $2 WHERE id = $3", - &[&new_name, &new_email, &user_id] - ).map_err(pg_err)?; + let mut query = String::from("UPDATE users SET "); + let mut updates = Vec::new(); + + // Owned parameters to avoid temporary borrow issues + + let mut owned_params: Vec<Box<dyn ToSql + Sync>> = Vec::new(); + + macro_rules! push_field { + ($field:expr, $value:expr) => { + if let Some(v) = $value { + updates.push(format!("{} = ${}", $field, owned_params.len() + 1)); + owned_params.push(Box::new(v)); + } + }; + } + + push_field!("name", new_name); + push_field!("email", new_email); + + if let Some(password) = new_password { + let password_hash = hash_password(password)?; + updates.push(format!("password_hash = ${}", owned_params.len() + 1)); + owned_params.push(Box::new(password_hash)); + } + + push_field!("username", new_username); + push_field!("studentid", new_studentid); + push_field!("startyear", new_startyear); + push_field!("endyear", new_endyear); + push_field!("profilepicture", new_profilepicture); + + if updates.is_empty() { + return Err(PyRuntimeError::new_err("No fields to update.")); + } + + query.push_str(&updates.join(", ")); + query.push_str(&format!(" WHERE id = ${}", owned_params.len() + 1)); + owned_params.push(Box::new(user_id)); + + let params: Vec<&(dyn ToSql + Sync)> = owned_params.iter().map(|b| &**b).collect(); + let updated = client.execute(query.as_str(), ¶ms).map_err(pg_err)?; + if updated == 0 { Err(PyRuntimeError::new_err("User not found")) } else { @@ -151,7 +212,7 @@ pub fn update_user_profile_picture(db_url: &str, user_id: i32, path: &str) -> Py pub fn update_first_login(db_url: &str, user_id: i32, first_login: bool) -> PyResult<()> { let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; let updated = client.execute( - "UPDATE users SET first_login = $1 WHERE id = $2", + "UPDATE users SET firstlogin = $1 WHERE id = $2", &[&first_login, &user_id], ).map_err(pg_err)?; if updated == 0 { diff --git a/rust_crud_api/src/lib.rs b/rust_crud_api/src/lib.rs index d0336b9006c00509aaa2872898bc0067a55b8069..6daa866acc7cb88bbd9eb89c270a01da6c62d320 100644 --- a/rust_crud_api/src/lib.rs +++ b/rust_crud_api/src/lib.rs @@ -1,8 +1,10 @@ mod models; mod auth; mod db; - +use db::update_user; use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + #[pymodule] fn rust_crud_api(_py: Python, m: &PyModule) -> PyResult<()> { @@ -24,6 +26,9 @@ fn rust_crud_api(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(db::users::update_user, m)?)?; m.add_function(wrap_pyfunction!(db::users::delete_user, m)?)?; m.add_function(wrap_pyfunction!(db::users::verify_user,m)?)?; + m.add_function(wrap_pyfunction!(db::users::update_first_login,m)?)?; + m.add_function(wrap_pyfunction!(db::users::update_user, m)?)?; + // Groups m.add_function(wrap_pyfunction!(db::groups::create_group, m)?)?; m.add_function(wrap_pyfunction!(db::groups::get_group, m)?)?;