From cefbebab33a2d8d01dfac4ba55920d4c22037a90 Mon Sep 17 00:00:00 2001 From: Emmy <07eman1@skola.svedala.se> Date: Tue, 2 Jun 2026 23:22:05 +0200 Subject: [PATCH] Upload files to "/" --- READ.md | 92 +++++++++++++++++++++++++++++++++++++++++++ app.py | 33 ++++++++++++++++ extensions.py | 23 +++++++++++ models.py | 66 +++++++++++++++++++++++++++++++ routes.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 READ.md create mode 100644 app.py create mode 100644 extensions.py create mode 100644 models.py create mode 100644 routes.py diff --git a/READ.md b/READ.md new file mode 100644 index 0000000..780f2b7 --- /dev/null +++ b/READ.md @@ -0,0 +1,92 @@ +#Weatherify + +Weatherify is an Flask-SQLAlchemy-applikation that creates Spotify playlists based on the weather of a location the users input. + +#Functions + +-Register and login system +-Creates playlists based on weatherdata +-Updates playlists with new tracks +-Deletes playlists + +#Technices +-Python +-Flask +-SQLAlchemy +-Flask-Login +-HTML / Jinja2 +-CSS +-JavaScript +-Spotify API +-OpenWeather API + +#Installion + +1. Clone project +´´´bash +git clone +cd weatherify + +2. Create virtuel enviorment + +´´´bash +python -m venv venv + +Activate enviorment +´´´bash +.venv\Scripts\activate + +3. Install requirements +´´´bash +pip install -requirements.txt +´´´ +4. Create one env-file + +Create an file which is called .env in projects rootmap with personal API-keys: + +´´´env +OPENWEATHER_API_KEY=your_openweather_key + +client_id=your_spotify_client_id +client_secret=your_spotify_client_secret +redirect_uri=http://127.0.0.1:5000/callback + +SECRET_KEY=your_secret_key +´´´ +5. Start application +´´´bash +flask run +´´´ +or + +´´´bash +python app.py +´´´ +6. Open in browser + +´´´ +http://127.0.0.1:5000 +´´´ + +#Database + +Project uses SQLAlchemy. + +if database does not exist: + +´´´bash +flask shell +´´´ + +```python +from extensions import db +db.create_all() +``` + +#Security + +-Password are hashed +-Protects from SQL-Injections because of SQLAchemy +-CRSF-protection on forms +-Authentication because of Flask-login + diff --git a/app.py b/app.py new file mode 100644 index 0000000..f796ffc --- /dev/null +++ b/app.py @@ -0,0 +1,33 @@ +from flask import Flask +from flask_migrate import Migrate +from flask_wtf.csrf import CSRFProtect +from dotenv import load_dotenv + +from extensions import db, login_manager +from playlist import bp as playlist_bp +from auth import bp as auth_bp + +load_dotenv() + +def create_app(): + app = Flask(__name__) + app.secret_key = "!spotifyweather1234!" + CSRFProtect(app) #aktivera csrf skydd i hela applikationen + + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db" + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + + db.init_app(app) + Migrate(app, db) + + login_manager.init_app(app) + + app.register_blueprint(playlist_bp) + app.register_blueprint(auth_bp) + + return app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/extensions.py b/extensions.py new file mode 100644 index 0000000..15ef219 --- /dev/null +++ b/extensions.py @@ -0,0 +1,23 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_bcrypt import Bcrypt + +db = SQLAlchemy() +bcrypt = Bcrypt() + +login_manager = LoginManager() + +#Protection for user-sessions +login_manager.session_protection = "strong" + +#Route users sends to if they are not logged in +login_manager.login_view = "auth.login_form" + +#Flash messages incase of unauthrized access +login_manager.login_message = "Login to continue" +login_manager.login_message_category = "error" + +@login_manager.user_loader +def load_user(user_id: str): + from models import User + return db.session.get(User, int(user_id)) \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..22231ea --- /dev/null +++ b/models.py @@ -0,0 +1,66 @@ +import re +from datetime import datetime + +from flask_login import UserMixin + +from extensions import db, bcrypt + +#Users +class User(UserMixin, db.Model): + __tablename__ = "user" + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), unique=True, nullable= False) + email = db.Column(db.String(120), unique=True, nullable = False) + password_hash = db.Column(db.String(200), nullable=False) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + + playlist = db.relationship("Playlist", back_populates="user", lazy="dynamic") + + def set_password(self, plain_password: str): + if not plain_password or len(plain_password) < 8: + raise ValueError("Password must be at least 8 characters!") + if not re.search(r"[A-Z]", plain_password): + raise ValueError("Password must include one uppercase letter") + if not re.search(r"[\d]", plain_password): + raise ValueError("Password must include one number") + + self.password_hash = ( + bcrypt.generate_password_hash( + plain_password + ).decode("utf-8") + ) + + def check_password(self, plain_password: str) -> bool: + return bcrypt.check_password_hash(self.password_hash, plain_password) + def __repr__(self): + return f"" +#Saves the playlist that the users makes +class Playlist(db.Model): + id= db.Column(db.Integer, primary_key=True) + + name = db.Column(db.String(100)) + description = db.Column(db.String(255)) + + spotify_playlist_id = db.Column(db.String(120)) + spotify_url = db.Column(db.String(300)) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + + user = db.relationship("User", backref="playlists") + tracks = db.relationship( + "Tracks", + backref ="playlist", + cascade="all, delete" + ) + +#Saves every song in the playlist +class Tracks(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255)) + artist = db.Column(db.String(255)) + + playlist_id = db.Column( + db.Integer, + db.ForeignKey("playlist.id") + ) + \ No newline at end of file diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..3616c2f --- /dev/null +++ b/routes.py @@ -0,0 +1,105 @@ +import re + +from flask import render_template, redirect, url_for, flash, request +from flask_login import login_user, logout_user, login_required, current_user +from sqlalchemy.exc import IntegrityError + + +from extensions import db +from models import User +from . import bp + +@bp.get("/register") +def register_form(): + if current_user.is_authenticated: + return redirect(url_for("playlist.home")) + return render_template("auth/register.html") + + +@bp.post("/register") +def register(): + username = request.form.get("username", "").strip() + email = request.form.get("email", "").strip().lower() + password = request.form.get("password", "").strip() + confirm = request.form.get("confirm", "").strip() + + if not all([username,email,password, confirm]): + flash("Fill in every field!", "error") + return redirect(url_for("auth.register_form")) + + email_regex = r"^[\w\.-]+@[\w\.-]+\.\w+$" + if not re.match(email_regex, email): + flash("invalid email address!", "error") + return redirect(url_for("auth.register")) + + if password != confirm: + flash("invalid password", "error") + return redirect(url_for("auth.register_form")) + + if not re.search(r"[A-Z]", password): + flash("invalid password", "error") + return redirect(url_for("auth.register")) + + if not re.search(r"[a-z]", password): + flash("invalid password", "error") + return redirect(url_for("register")) + + if not re.search(r"\d", password): + flash("invalid password", "error") + return redirect(url_for("auth.register")) + + exiting_user = User.query.filter_by(email=email).first() + + if exiting_user: + flash("User already exits!", "error") + return redirect(url_for("auth.register")) + + user = User(username=username, email=email) + + try: + user.set_password(password) + except ValueError as e: + flash(str(e), "error") + return redirect(url_for("auth.register_form")) + + try: + db.session.add(user) + db.session.commit() + except IntegrityError: + db.session.rollback() + flash("Username or email already registerd", "error") + return redirect(url_for("auth.register_form")) + + flash("Account has been created!", "success") + return redirect(url_for("auth.login_form")) + + +@bp.get("/login") +def login_form(): + if current_user.is_authenticated: + return redirect(url_for("playlist.home")) + return render_template("auth/login.html") + + +@bp.post("/login") +def login(): + email = request.form.get("email", "").strip().lower() + password = request.form.get("password", "") + + user = User.query.filter_by(email=email).first() + + if not user or not user.check_password(password): + flash("wrong email or password", "error") + return redirect(url_for("auth.login_form")) + + login_user(user) + + next_page = request.args.get("next") + return redirect(next_page or url_for("playlist.home")) + + +@bp.get("/logout") +def logout(): + logout_user() + flash("You are now logged out!", "error") + return redirect(url_for("auth.login_form"))