diff --git a/django_project/django_project/urls.py b/django_project/django_project/urls.py index 408ad439f7fb6443c3afc83897b80e141888df57..9fd572e0f973ad31be58a7453ca092289691c9f6 100644 --- a/django_project/django_project/urls.py +++ b/django_project/django_project/urls.py @@ -30,4 +30,6 @@ urlpatterns = [ path('groups/<int:group_id>/update/', views.group_update_view, name='group_update'), path('groups/<int:group_id>/delete/', views.group_delete_view, name='group_delete'), path('groups/<int:group_id>/', views.group_detail_view, name='group_detail'), + path('post/create/', views.create_post_view, name='create_post'), + path('feed/', views.feed_view, name='feed'), ] diff --git a/django_project/myapp/templates/myapp/create_post.html b/django_project/myapp/templates/myapp/create_post.html new file mode 100644 index 0000000000000000000000000000000000000000..33431a760065128ac47f14ffc74f82563a0acc5d --- /dev/null +++ b/django_project/myapp/templates/myapp/create_post.html @@ -0,0 +1,23 @@ +{% extends 'myapp/base.html' %} + +{% block title %}Create Post{% endblock %} + +{% block content %} +<h1>Create a New Post</h1> +{% if error %} + <p style="color: red;">{{ error }}</p> +{% endif %} +<form method="post"> + {% csrf_token %} + <label for="id_content">Content:</label><br> + <textarea name="content" id="id_content" rows="4" cols="50" required></textarea><br><br> + + <!-- Optional: for posting to a specific group --> + <label for="id_group_id">Group ID (optional):</label> + <input type="number" name="group_id" id="id_group_id"><br><br> + + <button type="submit">Post</button> +</form> +<p><a href="{% url 'account' %}">Back to Account</a></p> +{% endblock %} + diff --git a/django_project/myapp/templates/myapp/feed.html b/django_project/myapp/templates/myapp/feed.html new file mode 100644 index 0000000000000000000000000000000000000000..2fe9fec1538be953df73622b476716c535b39756 --- /dev/null +++ b/django_project/myapp/templates/myapp/feed.html @@ -0,0 +1,19 @@ +{% extends 'myapp/base.html' %} +{% block title %}Feed{% endblock %} +{% block content %} +<h1>Global Feed</h1> +{% if error %} + <p style="color:red;">{{ error }}</p> +{% endif %} +<ul> + {% for post in posts %} + <li> + <strong>{{ post.user_name }}</strong> posted at {{ post.created_at }}: + <p>{{ post.content }}</p> + </li> + {% empty %} + <li>No posts available.</li> + {% endfor %} +</ul> +{% endblock %} + diff --git a/django_project/myapp/views.py b/django_project/myapp/views.py index 2285b2f3b88075d57aaa77eb9614e07abf2da999..d5bf055563aaf9b4ed2c85aa5a81373045c59d10 100644 --- a/django_project/myapp/views.py +++ b/django_project/myapp/views.py @@ -233,3 +233,70 @@ def group_detail_view(request, group_id): except Exception as e: context['error'] = f"An error occurred: {e}" return render(request, 'myapp/group_detail.html', context) + +def account_view(request): + """ + Display the account page with user posts. + """ + db_url = settings.DATABASE_URL + user_id = request.session.get('user_id') + if not user_id: + return redirect('login') + + try: + # Retrieve user info, e.g., using get_user (or session data) + users = rust_crud_api.get_all_users(db_url) + user = next((u for u in users if u.id == user_id), None) + # Retrieve posts for this user (public posts) + posts = rust_crud_api.get_user_posts(db_url, user_id) + except Exception as e: + return render(request, 'myapp/account.html', {'error': str(e)}) + + return render(request, 'myapp/account.html', {'user': user, 'posts': posts}) + +def create_post_view(request): + """ + Allow a logged-in user to create a new post. + If a group is specified in the POST data, the post is associated with that group. + """ + db_url = settings.DATABASE_URL + user_id = request.session.get('user_id') + if not user_id: + return redirect('login') + + context = {} + if request.method == 'POST': + content = request.POST.get('content', '').strip() + # Optionally, get a group_id if the post is for a group: + group_id = request.POST.get('group_id') + if group_id: + try: + group_id = int(group_id) + except ValueError: + group_id = None + else: + group_id = None + + if not content: + context['error'] = "Post content cannot be empty." + return render(request, 'myapp/create_post.html', context) + + try: + rust_crud_api.create_post(db_url, user_id, group_id, content) + return redirect('account') + except Exception as e: + context['error'] = f"Error creating post: {str(e)}" + + return render(request, 'myapp/create_post.html', context) + +def feed_view(request): + """ + Display a global feed of posts. + """ + db_url = settings.DATABASE_URL + try: + posts = rust_crud_api.get_feed(db_url) + except Exception as e: + return render(request, 'myapp/feed.html', {'error': str(e)}) + + return render(request, 'myapp/feed.html', {'posts': posts}) diff --git a/rust_crud_api/Cargo.toml b/rust_crud_api/Cargo.toml index ef75e3ba299654186d7413aace2d9a1784967f49..081d59b898b154e4badc682278acd9b87a7dbdc7 100644 --- a/rust_crud_api/Cargo.toml +++ b/rust_crud_api/Cargo.toml @@ -15,3 +15,4 @@ serde_json = "1.0" jsonwebtoken = "8.2.0" argon2 = "0.4" rand = "0.8" +chrono = { version = "0.4", features = ["serde"]} diff --git a/rust_crud_api/src/db/init.rs b/rust_crud_api/src/db/init.rs index cf571d25c58fc1e88de75039f53c2698ac5cf6a0..cf4d1c594190f72ab97fc15692da5e82834eb44f 100644 --- a/rust_crud_api/src/db/init.rs +++ b/rust_crud_api/src/db/init.rs @@ -27,6 +27,13 @@ pub fn init_db(db_url: &str) -> PyResult<()> { user_id INTEGER REFERENCES users(id), PRIMARY KEY (group_id, user_id) ); + CREATE TABLE IF NOT EXISTS posts ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + group_id INTEGER REFERENCES groups(id), + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); " ).map_err(pg_err)?; Ok(()) diff --git a/rust_crud_api/src/db/mod.rs b/rust_crud_api/src/db/mod.rs index 3ed49cab3f0f523156e2842f7af76625e173b216..d718c042cc2b9a728aa4e1d19896c5f51af79bf5 100644 --- a/rust_crud_api/src/db/mod.rs +++ b/rust_crud_api/src/db/mod.rs @@ -1,8 +1,9 @@ pub mod users; pub mod groups; pub mod init; +pub mod posts; pub use init::init_db; pub use users::{create_user, get_user, get_all_users, update_user, delete_user}; pub use groups::{create_group, get_group, get_all_groups, add_user_to_group, get_group_members, delete_group}; - +pub use posts::{create_post, get_user_posts, get_group_posts, get_feed}; diff --git a/rust_crud_api/src/db/posts.rs b/rust_crud_api/src/db/posts.rs new file mode 100644 index 0000000000000000000000000000000000000000..5da267ae68f23e3ad0a149d60bb62195735acf9f --- /dev/null +++ b/rust_crud_api/src/db/posts.rs @@ -0,0 +1,81 @@ +use crate::models::Post; +use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; +use postgres::{Client, NoTls}; +use chrono::{DateTime, Utc}; +use std::time::SystemTime; + +fn pg_err(e: postgres::Error) -> PyErr { + PyRuntimeError::new_err(e.to_string()) +} + +/// Create a new post. If group_id is None, the post is for the user’s account or public feed. +/// Otherwise, it is posted to the specified group. +#[pyfunction(signature = (db_url, user_id, group_id=None, content=""))] +pub fn create_post(db_url: &str, user_id: i32, group_id: Option<i32>, content: &str) -> PyResult<()> { + let mut client = Client::connect(db_url, NoTls).map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + client.execute( + "INSERT INTO posts (user_id, group_id, content) VALUES ($1, $2, $3)", + &[&user_id, &group_id.as_ref(), &content] + ).map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + Ok(()) +} + +/// Retrieve posts for a given user. If group_id is None, returns posts that are not tied to a group, +/// which could be used for a public feed or the user’s own posts. +#[pyfunction] +pub fn get_user_posts(db_url: &str, user_id: i32) -> PyResult<Vec<String>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let rows = client.query( + "SELECT content FROM posts WHERE user_id = $1 AND group_id IS NULL ORDER BY created_at DESC", + &[&user_id] + ).map_err(pg_err)?; + + let posts = rows.iter().map(|row| row.get(0)).collect(); + Ok(posts) +} + +/// Retrieve posts for a given group. +#[pyfunction] +pub fn get_group_posts(db_url: &str, group_id: i32) -> PyResult<Vec<String>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let rows = client.query( + "SELECT content FROM posts WHERE group_id = $1 ORDER BY created_at DESC", + &[&group_id] + ).map_err(pg_err)?; + + let posts = rows.iter().map(|row| row.get(0)).collect(); + Ok(posts) +} + +/// Retrieve a global feed of posts (optionally filtering by user_id if needed). +#[pyfunction] +pub fn get_feed(db_url: &str) -> PyResult<Vec<Post>> { + let mut client = Client::connect(db_url, NoTls).map_err(pg_err)?; + let rows = client.query( + "SELECT p.id, p.user_id, u.name, p.content, p.created_at + FROM posts p + JOIN users u ON p.user_id = u.id + WHERE p.group_id IS NULL + ORDER BY p.created_at DESC", + &[] + ).map_err(pg_err)?; + + let mut posts = Vec::new(); + for row in rows { + let id: Option<i32> = row.get(0); + let user_id: i32 = row.get(1); + let user_name: Option<String> = Some(row.get(2)); + let content: String = row.get(3); + let created_at: SystemTime = row.get(4); // Retrieves timestamp from DB + let created_at = DateTime::<Utc>::from(created_at).to_rfc3339(); // Convert to String + posts.push(Post { + id, + user_id, + user_name, + content, + created_at, // Now a properly formatted String + }); + } + Ok(posts) +} diff --git a/rust_crud_api/src/lib.rs b/rust_crud_api/src/lib.rs index 7fcde253d953153ab01b0db9f5969a71e7bbcd24..d0336b9006c00509aaa2872898bc0067a55b8069 100644 --- a/rust_crud_api/src/lib.rs +++ b/rust_crud_api/src/lib.rs @@ -8,7 +8,7 @@ use pyo3::prelude::*; fn rust_crud_api(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::<models::User>()?; m.add_class::<models::Group>()?; - + m.add_class::<models::Post>()?; // Authentication functions m.add_function(wrap_pyfunction!(auth::generate_jwt, m)?)?; m.add_function(wrap_pyfunction!(auth::verify_jwt, m)?)?; @@ -17,19 +17,25 @@ fn rust_crud_api(_py: Python, m: &PyModule) -> PyResult<()> { // Database functions m.add_function(wrap_pyfunction!(db::init::init_db, m)?)?; + // Users m.add_function(wrap_pyfunction!(db::users::create_user, m)?)?; m.add_function(wrap_pyfunction!(db::users::get_user, m)?)?; m.add_function(wrap_pyfunction!(db::users::get_all_users, m)?)?; 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)?)?; + // Groups m.add_function(wrap_pyfunction!(db::groups::create_group, m)?)?; m.add_function(wrap_pyfunction!(db::groups::get_group, m)?)?; m.add_function(wrap_pyfunction!(db::groups::get_all_groups, m)?)?; m.add_function(wrap_pyfunction!(db::groups::add_user_to_group, m)?)?; m.add_function(wrap_pyfunction!(db::groups::get_group_members, m)?)?; m.add_function(wrap_pyfunction!(db::groups::delete_group, m)?)?; - + // Posts + m.add_function(wrap_pyfunction!(db::posts::create_post, m)?)?; + m.add_function(wrap_pyfunction!(db::posts::get_user_posts, m)?)?; + m.add_function(wrap_pyfunction!(db::posts::get_group_posts, m)?)?; + m.add_function(wrap_pyfunction!(db::posts::get_feed, m)?)?; Ok(()) } diff --git a/rust_crud_api/src/models/mod.rs b/rust_crud_api/src/models/mod.rs index e2723a9a2c3acd1f557621efec1008d00567cd7e..4c1fdd414302a354147f7f01a3dfd968c9f25011 100644 --- a/rust_crud_api/src/models/mod.rs +++ b/rust_crud_api/src/models/mod.rs @@ -1,6 +1,7 @@ pub mod user; pub mod group; +pub mod post; pub use user::User; pub use group::Group; - +pub use post::Post; diff --git a/rust_crud_api/src/models/post.rs b/rust_crud_api/src/models/post.rs new file mode 100644 index 0000000000000000000000000000000000000000..0bc20041ef157a82119802983a024f63f9a93551 --- /dev/null +++ b/rust_crud_api/src/models/post.rs @@ -0,0 +1,19 @@ +use pyo3::prelude::*; +use serde::{Serialize, Deserialize}; + +#[pyclass] +#[derive(Serialize, Deserialize, Debug)] +pub struct Post { + #[pyo3(get, set)] + pub id: Option<i32>, + #[pyo3(get,set)] + pub user_id: i32, + #[pyo3(get,set)] + pub user_name: Option<String>, + #[pyo3(get, set)] + pub content: String, + #[pyo3(get,set)] + pub created_at: String, + + +}