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