From 6e7e872a0b71882e536ec2e6b2d409328e4ecf88 Mon Sep 17 00:00:00 2001
From: James2Tulloch <146088090+James2Tulloch@users.noreply.github.com>
Date: Tue, 4 Mar 2025 13:47:47 +0000
Subject: [PATCH] Editing user profile information/ profile activation

---
 django_project/django_project/urls.py         |   4 +-
 django_project/myapp/.DS_Store                | Bin 6148 -> 6148 bytes
 .../myapp/templates/myapp/account.html        |   1 +
 .../myapp/templates/myapp/edit_account.html   |  63 +++++++++++
 .../myapp/templates/myapp/welcome_view.html   |  12 ++
 django_project/myapp/views/authviews.py       |  41 ++++---
 django_project/myapp/views/userviews.py       | 104 +++++++++++++++---
 rust_crud_api/src/db/init.rs                  |   4 +-
 rust_crud_api/src/db/users.rs                 |  79 +++++++++++--
 rust_crud_api/src/lib.rs                      |   7 +-
 10 files changed, 275 insertions(+), 40 deletions(-)
 create mode 100644 django_project/myapp/templates/myapp/edit_account.html
 create mode 100644 django_project/myapp/templates/myapp/welcome_view.html

diff --git a/django_project/django_project/urls.py b/django_project/django_project/urls.py
index ae506df..0e0131b 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
GIT binary patch
delta 27
jcmZoMXfc@J&&azmU^g=(?`9sBlgty#j5f1#{N)D#d9w()

delta 50
zcmZoMXfc@J&&aniU^g=(-)0_`lgykP3}p<N45<v|lLOeqco^0*Ffh#f4+fh!SOfrZ
Cpb(${

diff --git a/django_project/myapp/templates/myapp/account.html b/django_project/myapp/templates/myapp/account.html
index e741f80..d848047 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 0000000..6d94253
--- /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 0000000..f6bd6ad
--- /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 5ab6630..93aa837 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 0c5bedf..539c3e5 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 1c45983..045676d 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 ec15724..6c71f1d 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(), &params).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 d0336b9..6daa866 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)?)?;
-- 
GitLab