Upload files to "/"
This commit is contained in:
92
READ.md
Normal file
92
READ.md
Normal file
@@ -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 <repository-url>
|
||||||
|
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
|
||||||
|
|
||||||
33
app.py
Normal file
33
app.py
Normal file
@@ -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)
|
||||||
23
extensions.py
Normal file
23
extensions.py
Normal file
@@ -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))
|
||||||
66
models.py
Normal file
66
models.py
Normal file
@@ -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"<User id={self.id} username={self.username!r}>"
|
||||||
|
#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")
|
||||||
|
)
|
||||||
|
|
||||||
105
routes.py
Normal file
105
routes.py
Normal file
@@ -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"))
|
||||||
Reference in New Issue
Block a user