diff --git a/backend/models_shared.py b/backend/models_shared.py index 5f1e28c54d974eb8839da5aa75e3b955e637e889..d20fd998849230934b25571d5de682e81f9923fa 100644 --- a/backend/models_shared.py +++ b/backend/models_shared.py @@ -1,7 +1,7 @@ from flask_sqlalchemy import SQLAlchemy from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.types import Integer, String, Boolean, JSON +from sqlalchemy.types import Integer, String, Boolean, JSON, Text from typing import List @@ -46,7 +46,6 @@ class User(db.Model): authenticated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True) active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) - # Flask-Security user identifier fs_uniquifier: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) roles: Mapped[List["Role"]] = relationship() @@ -72,4 +71,10 @@ class SweDemographicResult(db.Model): __tablename__ = "swe_demographic_results" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) db_county_code: Mapped[int] = mapped_column(Integer, nullable=False) - db_data: Mapped[dict] = mapped_column(JSON, nullable=True) \ No newline at end of file + db_data: Mapped[dict] = mapped_column(JSON, nullable=True) + + +class Favorites(db.Model): + __tablename__ = "user_search_favorites" + user_fs_uniquifier: Mapped[str] = mapped_column(String(64), primary_key=True) + favorite_list: Mapped[str] = mapped_column(String(), nullable=False) diff --git a/backend/routes_auth.py b/backend/routes_auth.py index e6502398cdc9243a05b04e0c3f0b26d74e70b1d9..c275696b2a7cde93caf07ac7036cd1dfcc481cc0 100644 --- a/backend/routes_auth.py +++ b/backend/routes_auth.py @@ -1,6 +1,7 @@ from flask import Blueprint, current_app, jsonify, request -from flask_security import auth_required, logout_user -from models_shared import SweElectionResult, SweDemographicResult +from flask_security import auth_required, logout_user, current_user +from models_shared import SweElectionResult, SweDemographicResult, Favorites, db +import json auth = Blueprint("auth", __name__) @@ -71,4 +72,63 @@ def get_swe_demographic_data(): return jsonify({"info": response, "success": True}) except: - return jsonify({"errors" : ["County code is invalid"], "success" : False}) \ No newline at end of file + return jsonify({"errors" : ["County code is invalid"], "success" : False}) + + +@auth.route("/api/save_favorite", methods=["POST"]) +@auth_required() +def save_favorite(): + + data = request.get_json() # Get arguments from front-end (selectedOptions) + print("data") + print(data) + + # Check if there is an entry for this user + query_fav = Favorites.query.filter_by(user_fs_uniquifier=current_user.fs_uniquifier).first() + + if query_fav is None: + # Create an entry if this is the first time the user saves a favorite + db_entry = Favorites( + user_fs_uniquifier = current_user.fs_uniquifier, + favorite_list = json.dumps({'data' : [data]}) # parse the data to a json string + ) + db.session.add(db_entry) # Add the entry to be saved + db.session.commit() # Save + else: + current_dict = json.loads(query_fav.favorite_list) # Retrieve the entry and parse the json string + + existing_favorite_names = [] + for favorite in current_dict['data']: # current_dict['data'] is a list of dicts + if 'favoriteName' in favorite.keys(): + existing_favorite_names.append(favorite['favoriteName']) + + print("existing_favorite_names") + print(existing_favorite_names) + + if 'favoriteName' in data.keys(): + if data['favoriteName'] in existing_favorite_names: + print("Name exists") + return jsonify({"errors" : ["Name already exists"], "success" : False}) + + current_dict['data'].append(data) # Add the new favorite to the list + query_fav.favorite_list = json.dumps(current_dict) # Replace the previous list with the updated + db.session.commit() # Save + + return jsonify({"success": True}) + + +@auth.route("/api/get_favorites", methods=["GET"]) +@auth_required() +def get_favorites(): + + # Check if there is an entry for this user + query_fav = Favorites.query.filter_by(user_fs_uniquifier=current_user.fs_uniquifier).first() + if query_fav is None: + # This user has no favorites saved + return jsonify({"info": {}, "success": True}) + else: + parsed_info = json.loads(query_fav.favorite_list) # Retrieve the entry and parse the json string + # Only return the list is the entry + return jsonify({"info": parsed_info['data'], "success": True}) + + diff --git a/frontend/src/Components/Search/MultiLevelOptions.jsx b/frontend/src/Components/Search/MultiLevelOptions.jsx index 9cea0cc8a7c7b417b35d0b28c7285efe98fb8c43..710cebce8f4865297f4f51304e7c23d3237de483 100644 --- a/frontend/src/Components/Search/MultiLevelOptions.jsx +++ b/frontend/src/Components/Search/MultiLevelOptions.jsx @@ -1,4 +1,5 @@ import React, { useState, useContext, useEffect } from 'react'; +import axios from 'axios'; import '../../Css/MultiLevelOptions.css'; import { GlobalContext } from '../../Context/GlobalContext'; @@ -8,10 +9,34 @@ const MultiLevelOptions = () => { const [subCategory, setSubCategory] = useState(''); const [year, setYear] = useState(''); const [party, setParty] = useState(''); - const {selectedOptions, setSelectedOptions} = useContext(GlobalContext); + const [favoriteName, setFavoriteName] = useState(''); + const [favorites, setFavorites] = useState([]); + const [errorMessages, setErrorMessages] = useState([]); + const {serverAddress, serverPort, selectedOptions, setSelectedOptions} = useContext(GlobalContext); + const years = ['1973', '1976', '1979', '1982', '1985', '1988', '1991', '1994', '1998', '2002', '2006', '2010', '2014', '2018', '2022']; + const handleStoredFavorites = async () => { + + try { + const response = await axios.get(serverAddress + ':' + serverPort + '/api/get_favorites', { + withCredentials: true, // Include credentials (cookies) in the request + headers: { + 'Content-Type': 'application/json', + }, + }); + + // handle + console.log(response); + setFavorites(response.data.info); + + + } catch (error) { + console.error('Error fetching favorites:', error); + } + }; + // Handle category selection (Swedish Election / Swedish Demographic) const handleCategoryChange = (e) => { console.log("Handle Category Change = " + e.target.value) @@ -19,6 +44,11 @@ const MultiLevelOptions = () => { setSubCategory(''); setYear(''); setParty(''); + + // if the 'favorites' category is selected retrieve the saved favorites from the backend. + if (e.target.value === 'favorites') { + handleStoredFavorites(); + } }; // Handle subcategory selection (Total Votes / Party Votes) @@ -26,63 +56,121 @@ const MultiLevelOptions = () => { setSubCategory(e.target.value); setYear(''); setParty(''); + setFavoriteName(''); // clear the input field }; - // Handle year and party selection - const handleYearChange = (e) => setYear(e.target.value); - const handlePartyChange = (e) => setParty(e.target.value); + + const handleFavoriteChange = (e) => { + setSubCategory(e.target.value); + setYear(''); + setParty(''); + setFavoriteName(''); // clear the input field + + // The first element is 'Select Saved Favorite' and is not included + const index_of_favorite = e.target.options.selectedIndex - 1; + const selected_favorite_option = favorites[index_of_favorite]; + console.log("Index of fav = " + index_of_favorite); + console.log(selected_favorite_option); + setSelectedOptions(selected_favorite_option); + }; + + + const saveFavorite = async () => { + // Enrich with the name given by the user + var enrichedFavoriteOptions = {...selectedOptions}; // Hard copy + enrichedFavoriteOptions['favoriteName'] = favoriteName; + + // Send the current selected options and its name to backend + const response = await axios.post(serverAddress + ':' + serverPort + '/api/save_favorite', enrichedFavoriteOptions, { + withCredentials: true, // Include credentials (cookies) in the request + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.data.success) { + console.log(favoriteName); + setErrorMessages([]); + // After successfully saving, clear the input field + setFavoriteName(''); + } else { + console.log("error"); + console.log(favoriteName); + setErrorMessages(response.data.errors); + } + }; - // This will be called whenever any values in the array - // [category, subCategory, year, party] changes - useEffect(() => { - console.log("Category = " + category); - console.log("Sub-Category = " + subCategory); - console.log("year = "+ year); - console.log("party = " + party); + const getFavoriteDiv = () => { + return ( + <div className='favorite-div'> + <h1>Save as Favorite</h1> + <input type="text" placeholder="Input Favorite Name" + value={favoriteName} onChange={(e) => setFavoriteName(e.target.value)} + /> - if (category === '') { - setSelectedOptions({'option': ""}); // Reset since no option is selected + {/* Display error messages */} + {errorMessages.length > 0 && ( + <ul className="error-messages"> + {errorMessages.map((msg, index) => ( + <li key={index} className="error-message">{msg}</li> + ))} + </ul> + )} - } else if (category === 'Swedish Election Results' && subCategory === '') { - setSelectedOptions({'option': ""}); // Reset since no option is selected + <button onClick={saveFavorite}>Save Favorite</button> + </div> + ); + } + + // Handle year and party selection + const handleYearChange = (e) => setYear(e.target.value); + const handlePartyChange = (e) => setParty(e.target.value); - } else if (category === 'Swedish Election Results' && subCategory === 'partyVotes' && (!year || !party)) { - setSelectedOptions({'option': ""}); // Reset since no option is selected + // This will be called whenever any values in the array + // [category, subCategory, year, party] changes + useEffect(() => { - } else if (category === 'Swedish Demographic') { - // Route -> /api/swedish_demographic (note that .option is a part of the route) - setSelectedOptions({'option': "swedish_demographic"}); + console.log("Category = " + category); + console.log("Sub-Category = " + subCategory); + console.log("year = "+ year); + console.log("party = " + party); - } else if (category === 'Swedish Election Results' && subCategory === 'totalVotes') { - // Route -> /api/swedish_election_total_results (note that .option is a part of the route) - setSelectedOptions({'option': "swedish_election_total_results"}); + // Determine when options have been selected and when a request to the backend should be performed + if (category === '') { + setSelectedOptions({'option': ""}); // Reset since no option is selected - } else if (category === 'Swedish Election Results' && subCategory === 'allPartyVotes' && year){ - // Route -> /api/swedish_election_party_votes (note that .option is a part of the route) - console.log("Set corecct options"); - setSelectedOptions( - {'option': "swedish_election_party_votes", - 'subOption' : "everyParty_one_year", - 'year' : year - }); + } else if (category === 'Swedish Election Results' && subCategory === '') { + setSelectedOptions({'option': ""}); // Reset since no option is selected - }else if (category === 'Swedish Election Results' && subCategory === 'partyVotes' && year && party){ - // Route -> /api/swedish_election_party_votes (note that .option is a part of the route) - setSelectedOptions({ - 'option': "swedish_election_party_votes", - 'subOption' : false, - 'year': year, - 'party': party - }); - } + } else if (category === 'Swedish Election Results' && subCategory === 'partyVotes' && (!year || !party)) { + setSelectedOptions({'option': ""}); // Reset since no option is selected - }, [category, subCategory, year, party]); + } else if (category === 'Swedish Demographic') { + // Route -> /api/swedish_demographic (note that .option is a part of the route) + setSelectedOptions({'option': "swedish_demographic"}); - useEffect(() => { + } else if (category === 'Swedish Election Results' && subCategory === 'totalVotes') { + // Route -> /api/swedish_election_total_results (note that .option is a part of the route) + setSelectedOptions({'option': "swedish_election_total_results"}); + } else if (category === 'Swedish Election Results' && subCategory === 'allPartyVotes' && year){ + // Route -> /api/swedish_election_party_votes (note that .option is a part of the route) + setSelectedOptions( + {'option': "swedish_election_party_votes", + 'subOption' : "everyParty_one_year", + 'year' : year + }); - }, [handleCategoryChange]); + } else if (category === 'Swedish Election Results' && subCategory === 'partyVotes' && year && party){ + // Route -> /api/swedish_election_party_votes (note that .option is a part of the route) + setSelectedOptions({ + 'option': "swedish_election_party_votes", + 'subOption' : false, + 'year': year, + 'party': party + }); + }}, [category, subCategory, year, party]); // Rendering based on user selections @@ -95,13 +183,21 @@ const MultiLevelOptions = () => { <option value="">-</option> <option value="Swedish Election Results">Swedish Election</option> <option value="Swedish Demographic">Swedish Demographic</option> - </select> + <option value="favorites">Favorites</option> + + {/*add a favorite option here which invokes a request to backend for the logged on user, + backend should return saved searches. + + when calling setSelectedOptions above, also show a button on the screen which says + "save favorite". - {/* Second level menu, visible only if Swedish Election is selected */} - {category === 'Total Votes / Party Votes' && ( - <h1>Select Al</h1> - )} + Clicking this button should save the current search in the DB + I'd also think it would be good to be able to manually import and export favorites (in separate input fields) + */} + </select> + + {/* Second level menu, visible depending on first level menu selection */} {category === 'Swedish Election Results' && ( <select value={subCategory} onChange={handleSubCategoryChange}> <option value="">Select Subcategory</option> @@ -110,6 +206,22 @@ const MultiLevelOptions = () => { <option value="allPartyVotes">All Party Votes / Year</option> </select> )} + {category === 'favorites' && favorites.length === 0 && ( // No favorites saved + <select value={subCategory} onChange={handleSubCategoryChange}> + <option value="noFavSaved">No Favorites Saved</option> + </select> + )} + {category === 'favorites' && favorites.length > 0 && ( // Favorites saved + <select value={subCategory} onChange={handleFavoriteChange}> + <option value="">Select Saved Favorite</option> + {/* Dynamically draw the saved favorites*/} + {favorites.map((list_element, index) => ( + <option key={index} value={list_element.favoriteName} data_additional={list_element}> + {list_element.favoriteName} + </option> + ))} + </select> + )} {/* Third level menus: */} {subCategory === 'allPartyVotes' && ( @@ -153,17 +265,34 @@ const MultiLevelOptions = () => { {/* Final display based on selection */} <div> - {category && subCategory === '' && ( + {category === 'Swedish Demographic' && subCategory === '' && ( + <> <p>Selected: {category}</p> + {getFavoriteDiv()} + </> )} - {category && subCategory === 'totalVotes' && ( + {category === 'Swedish Election Results' && subCategory === 'totalVotes' && ( + <> <p>Showing Total Votes for {category}</p> + {getFavoriteDiv()} + </> )} - {category && subCategory === 'partyVotes' && year && party && ( + {category === 'Swedish Election Results' && subCategory === 'partyVotes' && year && party && ( + <> <p>Showing Votes for {party} in {year} Election</p> + {getFavoriteDiv()} + </> + )} + + {category === 'Swedish Election Results' && subCategory === 'allPartyVotes' && year && ( + <> + <p>Showing All Party Votes for the Year {year}</p> + {getFavoriteDiv()} + </> )} + </div> </div> ); diff --git a/frontend/src/Pages/Dashboard/Barchart.jsx b/frontend/src/Pages/Dashboard/Barchart.jsx index 3036b92cbb90e4e187661c55f629a21a69347f8f..78a18798802519b4dfcfcf90f1930a0d262b346f 100644 --- a/frontend/src/Pages/Dashboard/Barchart.jsx +++ b/frontend/src/Pages/Dashboard/Barchart.jsx @@ -289,7 +289,7 @@ const BarChart = ({ selectedKey }) => { .attr('stroke', 'white') // Set the color of the lines to white .attr('stroke-width', 0) // Set the width of the text .attr('fill', 'white') // Set the color of the text to white - attr('font-size', '20px'); + .attr('font-size', '20px'); // Create bars svg.selectAll('.bar') @@ -305,6 +305,7 @@ const BarChart = ({ selectedKey }) => { } }; + console.log("FetchData") fetchData(); console.log("Redraw Graph")