diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ee09d01 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,56 @@ +name: Backend Test + +on: [push, pull_request] + +jobs: + test: + + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:13 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: backend_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + # Wait until postgres is ready before running the tests + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r services/backend/requirements.txt + + - name: Create test database + run: | + psql -U postgres -c "CREATE DATABASE backend_test;" + + - name: Run tests + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/backend_dev + DATABASE_TEST_URL: postgres://postgres:postgres@localhost:5432/backend_test + APP_SETTINGS: project.config.TestingConfig + PYTHONPATH: services/backend + run: | + cd services/backend + pytest diff --git a/load_data.py b/load_data.py new file mode 100755 index 0000000..179b46d --- /dev/null +++ b/load_data.py @@ -0,0 +1,168 @@ +#!/usr/bin/python3 + +import argparse +import sqlalchemy +from sqlalchemy.dialects.postgresql import insert +from faker import Faker +import random +from tqdm import tqdm +import time +from datetime import datetime, timedelta + +fake = Faker() + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--db', required=True) + parser.add_argument('--event_rows', default=1000000, type=int) + parser.add_argument('--user_rows', default=1000000, type=int) + return parser.parse_args() + +def connect_database(db_url): + engine = sqlalchemy.create_engine(db_url, echo=False) + return engine + +# Load words from the English dictionary file +def load_dictionary(file_path='./services/postgres/words_alpha.txt'): + with open(file_path, 'r') as file: + words = file.read().splitlines() + return words + +dictionary_words = load_dictionary() + +def generate_event_name(event_type): + thematic_words = { + "Talk": ["Lecture", "Discussion", "Seminar", "Panel"], + "Festival": ["Gala", "Fest", "Celebration"], + "Awards Ceremony": ["Awards Night", "Recognition Gala", "Honors Evening"] + } + word = random.choice(thematic_words.get(event_type, ["Event"])) + return f"{fake.bs().title()} {word}" + +def generate_event_description(event_name): + # Select random words from the dictionary to create a longer description + random_words = ' '.join(random.choices(dictionary_words, k=20)) + contexts = [ + f"Join us for the {event_name}, an opportunity to engage with leading experts and enthusiasts from the industry. {random_words}", + f"This year's {event_name} features a series of immersive experiences designed to inspire and educate attendees. {random_words}", + f"Don't miss out on the {event_name}! It will be a gathering of minds and ideas that promises to be unforgettable. {random_words}" + ] + return random.choice(contexts) + +def generate_future_datetime(): + time_ranges = [ + ('next week', 7), + ('next two weeks', 14), + ('next month', 30), + ('within the next three months', 90), + ('within the next six months', 180) + ] + description, days = random.choice(time_ranges) + return fake.future_datetime(end_date=f"+{days}d") + +def insert_events(connection, num_events): + event_types = [ + "Talk", "Awards Ceremony", "Info Session", "Gala", "Screening", + "Colloquium", "Radio Play", "Class", "Lecture", "Festival" + ] + keywords = [ + "academics and graduate school", "networking and career development", "workshops and seminars", + "volunteering and fundraising", "affinity groups and cultural events", "activism and social justice", "athletics", + "wellness", "recreation and nightlife", "clubs and organizations", "science and technology", "arts and theater", + "food and snacks", "pre-professional events", "sustainability" + ] + sql = sqlalchemy.sql.text(""" + INSERT INTO events (name, description, location, start_time, end_time, organization, contact_information, registration_link, keywords, tsv) + VALUES (:name, :description, :location, :start_time, :end_time, :organization, :contact_information, :registration_link, :keywords, to_tsvector('english', :name || ' ' || :description)) + RETURNING id_events; + """) + event_ids = [] + for _ in tqdm(range(num_events), desc="Inserting events"): + event_type = random.choice(event_types) + event_name = generate_event_name(event_type) + description = generate_event_description(event_name) + start_time = generate_future_datetime() + end_time = start_time + timedelta(hours=random.choice([1, 2, 3, 4, 5, 6])) + event_keywords = random.sample(keywords, k=random.randint(1, 5)) # Select 1-5 random keywords + event = { + 'name': event_name, + 'description': description, + 'location': fake.address(), + 'start_time': start_time, + 'end_time': end_time, + 'organization': fake.company(), + 'contact_information': fake.phone_number(), + 'registration_link': fake.url(), + 'keywords': event_keywords + } + try: + result = connection.execute(sql, event) + event_id = result.fetchone()[0] + event_ids.append(event_id) + except sqlalchemy.exc.IntegrityError as e: + print(f"Failed to insert event: {e}") + return event_ids + +def generate_users(connection, num_users): + sql = sqlalchemy.sql.text(""" + INSERT INTO users (email, password) + VALUES (:email, :password) + RETURNING id_users; + """) + user_ids = [] + for _ in tqdm(range(num_users), desc="Inserting users"): + user = { + 'email': fake.email(), + 'password': fake.password() + } + try: + result = connection.execute(sql, user) + user_id = result.fetchone()[0] + user_ids.append(user_id) + except sqlalchemy.exc.IntegrityError as e: + print(f"Failed to insert user: {e}") + return user_ids + +def insert_user_to_events(connection, user_ids, event_ids): + sql = sqlalchemy.sql.text(""" + INSERT INTO user_to_events (user_id, event_id) + VALUES (:user_id, :event_id); + """) + num_users = len(user_ids) + num_events = len(event_ids) + popular_event_ids = random.choices(event_ids, k=int(num_events * 0.1)) # 10% of events are way more favorited + for user_id in tqdm(user_ids, desc="Inserting user_to_events"): + # Each user favorites 10 events + favorite_event_ids = random.sample(event_ids, k=5) + random.sample(popular_event_ids, k=5) + for event_id in favorite_event_ids: + try: + connection.execute(sql, {'user_id': user_id, 'event_id': event_id}) + except sqlalchemy.exc.IntegrityError as e: + print(f"Failed to insert user_to_event: {e}") + +def insert_data(engine, num_events, num_users): + with engine.connect() as connection: + transaction = connection.begin() + try: + event_ids = insert_events(connection, num_events) + user_ids = generate_users(connection, num_users) + insert_user_to_events(connection, user_ids, event_ids) + transaction.commit() + except Exception as e: + print(f"An error occurred: {e}") + transaction.rollback() + finally: + connection.close() + +def main(): + args = parse_args() + engine = connect_database(args.db) + start_time = time.time() + + insert_data(engine, args.event_rows, args.user_rows) + + end_time = time.time() + print('Runtime =', end_time - start_time) + +if __name__ == "__main__": + main() diff --git a/load_data.sh b/load_data.sh new file mode 100755 index 0000000..c5b0e23 --- /dev/null +++ b/load_data.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +echo 'loading data' + +time python3 load_data.py --db=postgresql://postgres:postgres@localhost:5435/backend_dev --user_rows=1000000 --event_rows=1000000 + +echo 'finished loading' \ No newline at end of file diff --git a/services/backend/project/__init__.py b/services/backend/project/__init__.py index 9f361c9..b77137a 100644 --- a/services/backend/project/__init__.py +++ b/services/backend/project/__init__.py @@ -4,6 +4,8 @@ from datetime import datetime, timedelta from random import choice from flask import Flask, jsonify, request, make_response, session, redirect, url_for +from datetime import datetime +from flask_sqlalchemy import SQLAlchemy from sqlalchemy import create_engine, text from flask_cors import CORS from flask_socketio import SocketIO @@ -17,6 +19,7 @@ app.config.from_object(os.getenv('APP_SETTINGS', 'project.config.DevelopmentConfig')) engine = create_engine(app.config['DATABASE_URI']) +db = SQLAlchemy(app) @app.route('/login', methods=['POST']) def login(): @@ -78,9 +81,9 @@ def create_user(): @app.route('/all_users', methods=['GET']) def get_users(): with engine.connect() as connection: - users = connection.execute(text("SELECT * FROM users")).fetchall() - users_list = [{'id': user.id_users, 'email': user.email} for user in users] - return jsonify(users_list) + users = connection.execute(text("SELECT id_users, email FROM users")).fetchall() + users_list = [{'id_users': user.id_users, 'email': user.email} for user in users] + return jsonify(users_list) #get user by id @@ -91,7 +94,7 @@ def get_user(user_id): if user is None: return jsonify({'message': 'User not found'}), 404 user_data = { - 'id_users': user.id, + 'id_users': user.id_users, 'email': user.email, 'password': user.password } @@ -105,10 +108,10 @@ def create_event(): sql = """ INSERT INTO events (name, description, location, start_time, end_time, organization, - contact_information, registration_link, keywords) + contact_information, registration_link, keywords, tsv) VALUES (:name, :description, :location, :start_time, :end_time, :organization, - :contact_information, :registration_link, :keywords) + :contact_information, :registration_link, :keywords, to_tsvector('english', :name || ' ' || :description)) RETURNING id_events; """ @@ -135,15 +138,26 @@ def create_event(): @app.route('/all_events', methods=['GET']) def get_events(): + page = request.args.get('page', 1, type=int) + per_page = 20 + offset = (page - 1) * per_page + with engine.connect() as connection: - events = connection.execute(text("SELECT * FROM events")).fetchall() - events_list = [{'id': event.id_events, - 'name': event.name, + current_time = datetime.utcnow() + events = connection.execute(text(""" + SELECT * FROM events + WHERE start_time >= :current_time + ORDER BY start_time + LIMIT :per_page OFFSET :offset + """), {'current_time': current_time, 'per_page': per_page, 'offset': offset}).fetchall() + + events_list = [{'id': event.id_events, + 'name': event.name, 'description': event.description, - 'location': event.location, - 'start_time': event.start_time, + 'location': event.location, + 'start_time': event.start_time, 'end_time': event.end_time, - 'organization': event.organization, + 'organization': event.organization, 'contact_information': event.contact_information, 'registration_link': event.registration_link, 'keywords': event.keywords} for event in events] @@ -207,6 +221,46 @@ def events_by_user(user_id): return jsonify(events_list) +@app.route('/events_by_favorites', methods=['GET']) +def get_top_favorited_events(): + try: + # SQL query to select the top 10 most favorited events + query = """ + SELECT e.id_events, e.name, e.description, e.location, e.start_time, e.end_time, + e.organization, e.contact_information, e.registration_link, e.keywords, + COUNT(ue.event_id) AS likes + FROM events e + LEFT JOIN user_to_events ue ON e.id_events = ue.event_id + GROUP BY e.id_events + ORDER BY likes DESC + LIMIT 10 + """ + with engine.connect() as connection: + # Execute the SQL query + events = connection.execute(text(query)).fetchall() + if not events: + return jsonify({'message': 'No events found'}), 404 + + # Convert the result to a list of dictionaries for JSON serialization + events_list = [{'id': event.id_events, + 'name': event.name, + 'description': event.description, + 'location': event.location, + 'start_time': event.start_time, + 'end_time': event.end_time, + 'organization': event.organization, + 'contact_information': event.contact_information, + 'registration_link': event.registration_link, + 'keywords': event.keywords, + 'favorites': event.likes + } for event in events] + + return jsonify(events_list) + except Exception as e: + # Handle exceptions gracefully + return jsonify({'error': str(e)}), 500 + + @app.route('/toggle_user_event', methods=['POST']) def toggle_user_event(): data = request.json @@ -243,6 +297,61 @@ def toggle_user_event(): """), {'user_id': user_id, 'event_id': event_id}) connection.commit() return jsonify({'message': 'User added to event successfully', 'isFavorited': True}), 201 + + # @app.route(get favorites by event) + # view friends on about page + + # most recent messages view, prev next toggle buttons + +@app.route('/search', methods=['GET']) +def search(): + query = request.args.get('query', '').strip() + page = request.args.get('page', 1, type=int) + per_page = 20 + offset = (page - 1) * per_page + + with engine.connect() as connection: + if query: + # FTS query + search_query = text(""" + SELECT id_events, + ts_headline('english', name, plainto_tsquery(:query)) AS name, + ts_headline('english', description, plainto_tsquery(:query)) AS description, + location, start_time, end_time, organization, contact_information, + registration_link, keywords + FROM events + WHERE to_tsvector('english', name || ' ' || description) @@ plainto_tsquery(:query) + ORDER BY ts_rank(to_tsvector('english', name || ' ' || description), plainto_tsquery(:query)) DESC + LIMIT :per_page OFFSET :offset; + """) + events = connection.execute(search_query, {'query': query, 'per_page': per_page, 'offset': offset}).fetchall() + + # Spelling suggestion query + suggestions_query = text(""" + SELECT suggestion FROM get_spelling_suggestions(:query); + """) + suggestions = connection.execute(suggestions_query, {'query': query}).fetchall() + else: + # If query is empty, return all events + events_query = text(""" + SELECT id_events, name, description, location, start_time, end_time, organization, contact_information, + registration_link, keywords + FROM events + ORDER BY start_time + LIMIT :per_page OFFSET :offset; + """) + events = connection.execute(events_query, {'per_page': per_page, 'offset': offset}).fetchall() + suggestions = [] + + events_list = [{'id': event.id_events, 'name': event.name, 'description': event.description, + 'location': event.location, 'start_time': event.start_time, 'end_time': event.end_time, + 'organization': event.organization, 'contact_information': event.contact_information, + 'registration_link': event.registration_link, 'keywords': event.keywords} + for event in events] + suggestions_list = [suggestion.suggestion for suggestion in suggestions] + + return jsonify({'events': events_list, 'suggestions': suggestions_list}) + if __name__ == '__main__': diff --git a/services/backend/project/__pycache__/__init__.cpython-311.pyc b/services/backend/project/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..047ed6e Binary files /dev/null and b/services/backend/project/__pycache__/__init__.cpython-311.pyc differ diff --git a/services/backend/project/__pycache__/config.cpython-311.pyc b/services/backend/project/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..dcf073c Binary files /dev/null and b/services/backend/project/__pycache__/config.cpython-311.pyc differ diff --git a/services/backend/project/config.py b/services/backend/project/config.py index fc98669..974dff1 100644 --- a/services/backend/project/config.py +++ b/services/backend/project/config.py @@ -5,17 +5,21 @@ class BaseConfig: """Base configuration""" TESTING = False + SQLALCHEMY_TRACK_MODIFICATIONS = False class DevelopmentConfig(BaseConfig): """Development configuration""" DATABASE_URI = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@backend-db:5432/backend_dev") + SQLALCHEMY_DATABASE_URI = DATABASE_URI class TestingConfig(BaseConfig): """Testing configuration""" TESTING = True - DATABASE_URI = os.getenv("DATABASE_TEST_URL", "postgresql://postgres:postgres@backend-db:5432/backend_test") + DATABASE_URI = os.getenv("DATABASE_TEST_URL", "postgresql://postgres:postgres@localhost:5435/backend_test") + SQLALCHEMY_DATABASE_URI = DATABASE_URI class ProductionConfig(BaseConfig): """Production configuration""" DATABASE_URI = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@backend-db:5432/backend_prod") + SQLALCHEMY_DATABASE_URI = DATABASE_URI diff --git a/services/backend/tests/__pycache__/conftest.cpython-311-pytest-8.2.0.pyc b/services/backend/tests/__pycache__/conftest.cpython-311-pytest-8.2.0.pyc new file mode 100644 index 0000000..03e535d Binary files /dev/null and b/services/backend/tests/__pycache__/conftest.cpython-311-pytest-8.2.0.pyc differ diff --git a/services/backend/tests/__pycache__/test_routes.cpython-311-pytest-8.2.0.pyc b/services/backend/tests/__pycache__/test_routes.cpython-311-pytest-8.2.0.pyc new file mode 100644 index 0000000..b7ef82f Binary files /dev/null and b/services/backend/tests/__pycache__/test_routes.cpython-311-pytest-8.2.0.pyc differ diff --git a/services/backend/tests/conftest.py b/services/backend/tests/conftest.py new file mode 100644 index 0000000..99eee12 --- /dev/null +++ b/services/backend/tests/conftest.py @@ -0,0 +1,53 @@ +import os +import sys +import pytest +from sqlalchemy import text +from project import app, db + +# Add the project directory to the sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) + +# Ensure environment variables are set correctly +os.environ['APP_SETTINGS'] = 'project.config.TestingConfig' +os.environ['DATABASE_URL'] = 'postgresql://postgres:postgres@localhost:5435/backend_dev' +os.environ['DATABASE_TEST_URL'] = 'postgresql://postgres:postgres@localhost:5435/backend_test' + +print("APP_SETTINGS:", os.getenv('APP_SETTINGS')) +print("DATABASE_URL:", os.getenv('DATABASE_URL')) +print("DATABASE_TEST_URL:", os.getenv('DATABASE_TEST_URL')) + +def apply_schema(engine): + schema_path = os.path.join(os.path.dirname(__file__), '../../postgres/schema.sql') + with engine.connect() as connection: + with open(schema_path) as schema_file: + schema_sql = schema_file.read() + connection.execute(text(schema_sql)) + +@pytest.fixture(scope='session') +def test_app(): + app.config.from_object('project.config.TestingConfig') + print("SQLALCHEMY_DATABASE_URI:", app.config['SQLALCHEMY_DATABASE_URI']) + + with app.app_context(): + db.create_all() # Ensure the tables are created + apply_schema(db.engine) # Apply the schema from the schema.sql file + yield app + db.drop_all() # Drop the tables after the test session + +@pytest.fixture(scope='session') +def client(test_app): + return test_app.test_client() + +@pytest.fixture(scope='function') +def init_database(): + with app.app_context(): + with db.engine.connect() as connection: + connection.execute(text("INSERT INTO users (email, password) VALUES ('testuser@example.com', 'password123')")) + connection.execute(text(""" + INSERT INTO events (name, description, location, start_time, end_time, organization, contact_information, registration_link, keywords) + VALUES ('Test Event', 'This is a test event', 'Test Location', '2024-05-09T00:00:00', '2024-05-09T01:00:00', 'Test Org', 'contact@test.org', 'http://test.org/register', ARRAY['test']) + """)) + yield + connection.execute(text("DELETE FROM users")) + connection.execute(text("DELETE FROM events")) + connection.execute(text("DELETE FROM user_to_events")) diff --git a/services/backend/tests/test_routes.py b/services/backend/tests/test_routes.py new file mode 100644 index 0000000..b9e4b7e --- /dev/null +++ b/services/backend/tests/test_routes.py @@ -0,0 +1,77 @@ +import pytest +from sqlalchemy import text +from project import db + +def client(app): + """A test client for the Flask app.""" + return app.test_client() + + +def teardown_function(): + # Clear the database after each test + with db.engine.connect() as connection: + connection.execute(text("DELETE FROM users")) + connection.execute(text("DELETE FROM events")) + connection.execute(text("DELETE FROM user_to_events")) + +def test_create_user(client): + response = client.post('/create_user', json={ + 'email': 'newuser@example.com', + 'password': 'newpassword123' + }) + assert response.status_code == 201 + data = response.get_json() + assert data['message'] == 'User created successfully' + +def test_create_event(client): + """Test the create_event endpoint.""" + sample_data = { + 'name': 'Sample Event', + 'description': 'This is a sample event description.', + 'location': 'Sample Location', + 'start_time': '2024-05-09 12:00:00', + 'end_time': '2024-05-09 15:00:00', + 'organization': 'Sample Organization', + 'contact_information': 'sample@example.com', + 'registration_link': 'https://example.com/register', + 'keywords': ['sample, event, testing'] + } + + # Simulate a POST request to the create_event endpoint with sample data + response = client.post('/create_event', json=sample_data) + + # Check if the response status code is 200 + assert response.status_code == 200 + + # Check if the response contains the expected message and event ID + assert b'Event created successfully' in response.data + assert b'eventID' in response.data + +def test_get_event(client): + response = client.get('/get_event/1') + assert response.status_code == 200 + data = response.get_json() + assert data['name'] == 'Sample Event' + +def test_login(client): + response = client.post('/login', json={ + 'email': 'newuser@example.com', + 'password': 'newpassword123' + }) + assert response.status_code == 200 + data = response.get_json() + assert data['success'] == True + + +def test_get_users(client): + response = client.get('/all_users') + assert response.status_code == 200 + +def test_logout(client): + response = client.post('/logout') + assert response.status_code == 200 + data = response.get_json() + assert data['message'] == 'You have been logged out' + + + diff --git a/services/client/my-app/src/AboutPage.js b/services/client/my-app/src/AboutPage.js index e27fcea..b9070de 100644 --- a/services/client/my-app/src/AboutPage.js +++ b/services/client/my-app/src/AboutPage.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import './App.css'; // Import the app.css file import ProfileIcon from './ProfileIcon'; @@ -9,31 +9,51 @@ import profileimg from './profileimg.png'; const imageUrl = profileimg; function AboutPage() { + const [users, setUsers] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch('http://localhost:5001/all_users') + .then(response => response.json()) + .then(data => { + setUsers(data); + setIsLoading(false); + }) + .catch(error => { + setError(error.message); + setIsLoading(false); + }); + }, []); + return ( -
P-5cEvents is a web app designed to organize events and calendars, offering comprehensive event scheduling assistance
-Abrar Yaser POM '25
- -Description: {event.description}
-Location: {event.location}
-Start Time: {new Date(event.start_time).toLocaleString()}
-End Time: {new Date(event.end_time).toLocaleString()}
-Organization: {event.organization}
-Contact Information: {event.contact_information}
-Registration Link: {event.registration_link}
-Keywords: {event.keywords.join(', ')}
-Description:
+Location: {event.location}
+Start Time: {new Date(event.start_time).toLocaleString()}
+End Time: {new Date(event.end_time).toLocaleString()}
+Organization: {event.organization}
+Contact Information: {event.contact_information}
+Registration Link: {event.registration_link}
+Keywords: {event.keywords.join(', ')}
+Favorites: {event.favorites}
+Description:
+Location: {event.location}
+Start Time: {new Date(event.start_time).toLocaleString()}
+End Time: {new Date(event.end_time).toLocaleString()}
+Organization: {event.organization}
+Contact Information: {event.contact_information}
+Registration Link: {event.registration_link}
+Keywords: {event.keywords.join(', ')}
+