diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cdbbaa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +media/2024-11-23 11-49-Rowan.pcmp +media/2024-11-23 18-11-Gabriella.pcmp +media/2024-11-24 09-14-Default.pcmp +media/2024-11-24 11-04-Gabriella.pcmp +media/2024-11-24 19-44-Rowan.pcmp +media/2025-09-06 17-56-Gabriella.pcmp +media/2025-09-06 19-06-Rowan.pcmp +instance/db.sqlite +static/avatars/0d1c5f6dbb1d44c59f43c7d10cd3f9d3_l.png +static/avatars/0d1c5f6dbb1d44c59f43c7d10cd3f9d3_m.png +static/avatars/0d1c5f6dbb1d44c59f43c7d10cd3f9d3_s.png +static/avatars/1c1e283bc3974a338306df55809c3eb9_l.png +static/avatars/1c1e283bc3974a338306df55809c3eb9_m.png +static/avatars/1c1e283bc3974a338306df55809c3eb9_s.png +static/avatars/3e9717b8a2484c36801c983a7c98389e_raw.png +static/avatars/6f9d062c23774444bf97b30768aa7e3f_raw.png +static/avatars/8d7ae602f4e744e2b0de0cad75280c4b_l.png +static/avatars/8d7ae602f4e744e2b0de0cad75280c4b_m.png +static/avatars/8d7ae602f4e744e2b0de0cad75280c4b_s.png +static/avatars/8de5d46847d243b7946fb987c1d7a158_raw.png +static/avatars/9d7056cd58604367b3da797d47bf7de3_raw.png +static/avatars/9df04f769ebd4b8298198abbbd4696ee_l.png +static/avatars/9df04f769ebd4b8298198abbbd4696ee_m.png +static/avatars/9df04f769ebd4b8298198abbbd4696ee_s.png +static/avatars/12a6ce4e6eb9455c9fd300bc5bdcc68f_raw.png +static/avatars/13d09f763cf5423c93c476475e6fedb5_raw.png +static/avatars/39fb0118e7c247abbe5104ad039332c6_raw.png +static/avatars/071c522d704d42eebad3114439d01548_l.png +static/avatars/071c522d704d42eebad3114439d01548_m.png +static/avatars/071c522d704d42eebad3114439d01548_s.png +static/avatars/79ccc4d8993b4d7297f9ec0e01245fa4_raw.png +static/avatars/613fc5aac72c42c9b9f034d039098787_l.png +static/avatars/613fc5aac72c42c9b9f034d039098787_m.png +static/avatars/613fc5aac72c42c9b9f034d039098787_s.png +static/avatars/684ed5cda7bd4eb08decee8d3f5ceb33_raw.png +static/avatars/747c34da7f59464fa1e19856c6263c6b_l.png +static/avatars/747c34da7f59464fa1e19856c6263c6b_m.png +static/avatars/747c34da7f59464fa1e19856c6263c6b_s.png +static/avatars/956ff2dbbc8b485d9a4b2a509d67b195_raw.png +static/avatars/5041d223f89940e3ad0546baed22f99c_raw.png +static/avatars/9037d37a43f54163b0322a9c4b6e335a_l.png +static/avatars/9037d37a43f54163b0322a9c4b6e335a_m.png +static/avatars/9037d37a43f54163b0322a9c4b6e335a_s.png +static/avatars/9139f11ea5854a6bb86d5a9f38e07ac4_raw.png +static/avatars/aed55274f1c84b52bab1b30ab3adfdb2_l.png +static/avatars/aed55274f1c84b52bab1b30ab3adfdb2_m.png +static/avatars/aed55274f1c84b52bab1b30ab3adfdb2_s.png +static/avatars/ba03d8c967ef458b9069b9367f9a7a63_raw.png +static/avatars/ba72f533f5c94dd79dde64ee5aca4fa1_l.png +static/avatars/ba72f533f5c94dd79dde64ee5aca4fa1_m.png +static/avatars/ba72f533f5c94dd79dde64ee5aca4fa1_s.png +static/avatars/ba79862152174438aff256bdab9aacc1_l.png +static/avatars/ba79862152174438aff256bdab9aacc1_m.png +static/avatars/ba79862152174438aff256bdab9aacc1_s.png +static/avatars/be2d767474e3406ba2463acd65f03158_l.png +static/avatars/be2d767474e3406ba2463acd65f03158_m.png +static/avatars/be2d767474e3406ba2463acd65f03158_s.png +static/avatars/c7b11bc3d7fd4993ad03b5e9046986c4_raw.png +static/avatars/c24f0d67bb9f4ef99fe7eac3e135d8e7_l.png +static/avatars/c24f0d67bb9f4ef99fe7eac3e135d8e7_m.png +static/avatars/c24f0d67bb9f4ef99fe7eac3e135d8e7_s.png +static/avatars/c9325959a1e940c184e4e597687211ae_l.png +static/avatars/c9325959a1e940c184e4e597687211ae_m.png +static/avatars/c9325959a1e940c184e4e597687211ae_s.png +static/avatars/de8bb1c2465345f3bc6a05c560ff743d_raw.png +static/avatars/e95939dcb19544a3aab8a88e806a09b1_raw.png +static/avatars/f6b5d00609a74f7980b0e15958783da3_raw.png +static/images/2024-03-03 225803255514-gen-image-HJpDVk.jpg +static/images/Gabbie Picture 2.png +static/versions/alpha_v1.0.zip +static/versions/latest.zip diff --git a/database.py b/database.py new file mode 100644 index 0000000..fb45b84 --- /dev/null +++ b/database.py @@ -0,0 +1,18 @@ +import easySQL, pathlib + +@easySQL.Table +class Uploads(): + def __init__(self): + self.name = "logins" + self.columns = { + "userid": easySQL.INTEGER, + "filename": easySQL.STRING, + "author": easySQL.STRING, + "version": easySQL.STRING, + "path": easySQL.STRING + } + +def add_upload(table, userid, filename, author, version, path): + easySQL.insert_into_table(table, + [userid, filename, author, version, path] + ) \ No newline at end of file diff --git a/easySQL.py b/easySQL.py new file mode 100644 index 0000000..9489b7a --- /dev/null +++ b/easySQL.py @@ -0,0 +1,243 @@ +from typing import Any +import sqlite3, pathlib +from collections import namedtuple + +STRING = 'string' +INTEGER = 'integer' +UNIQUE = 'UNIQUE' +JSON = 'string' # TODO: create a function for converting lists and dict into json string and back +database = None + +# TODO: add functionality to seek all entries in a filter that CONTAINS a string + + +def VALDATED_STRING(): + return 'string' + +def intergrate(database_path: pathlib.Path = None) -> sqlite3.Connection: + + if not database_path: + database_path = pathlib.Path("test.sqlite") + + global database + database = sqlite3.connect(database=database_path.absolute()) + return database + + +def Table(cls): + """ easySQL decorator for table classes for easy instantiation of many of the SQL_execute strings. + + This class will always need these variables defined within its __init__ method; + + self.name = "foo"; This will be the name of the table in the integrated database + self.columns = {foo: dah, ...}; dictionary of foo being the column name, and dah being the columns type in the database + types for a column are: + - STRING + - INTEGER + - UNIQuE + - JSON + + Returns: + Table: returns a Table class wrapped around the original class. + """ + class Table(cls): + def __init__(self, *args, **kwargs) -> None: + super(Table, self).__init__(*args, **kwargs) + self.data_object = namedtuple(f"{self.name}_row", list(self.columns.keys())) + self.columns_validation = len(self.columns) + + def __repr__(self): + return f"{self.__class__.__name__} ('{self.name}')" + + @property + def create_table(self): + def manufacture_create_SQL_string() -> str: + """ Takes the super()'s columns dictionary and bulds parts of the SQL_execute string. + + Returns: + str: middle of create table SQL_execute string. + """ + # TODO: very crude way of doing it, research a better way. + middle_string = 'id integer PRIMARY KEY, ' + current_count = 0 + for column_name, column_type in self.columns.items(): + if current_count == len(self.columns.items())-1: + middle_string += f"{column_name} {column_type}" + else: + middle_string += f"{column_name} {column_type}, " + current_count += 1 + return middle_string + return f"CREATE TABLE {self.name} ({manufacture_create_SQL_string()});" + + @property + def drop_table(self): + return f"DROP TABLE {self.name};" + + def select_row(self, column: str = None, match = None): + if column: + return f"SELECT * FROM {self.name} WHERE {column}= '{match}'" + else: + return f"SELECT * FROM {self.name}" + + def insert_row(self, data) -> namedtuple: + + def manufacture_insert_SQL_String(): + middle_string = '(' + gavel_string = '(' + current_count = 0 + for column_name in self.columns.keys(): + if current_count == len(self.columns.items())-1: + middle_string += f"{column_name})" + gavel_string += f"?)" + else: + middle_string += f"{column_name}, " + gavel_string += f"?, " + current_count += 1 + return f"{middle_string} VALUES {gavel_string}" + + query = namedtuple('Query', ['query', 'data']) + if len(data) == self.columns_validation: + return query(query=f"INSERT INTO {self.name}{manufacture_insert_SQL_String()}", data=data) + else: + return query(query=False, data= f"passed data to {self.name} is not the right length of entries") + + def update_row_by_id(self, data: dict, id: str): + """ Update a row at {id} with {data}. + + Args: + data (dict): key = column, value = data to update to + id (str): row_id in Table + """ + def manufactur_update_SQL_string(data: dict) -> str: + """ takes data and builds a SQL_execute string segment + + Args: + data (dict): Key = column, value = data to update to + + Returns: + _type_: middle segment of SQL_execute string + """ + # TODO: this is a very crude implementtion + middle_string = '' + current_count = 0 + for key, value in data.items(): + if current_count == len(data.items())-1: + middle_string += f" {key} = '{value}'" + else: + middle_string += f" {key} = '{value}', " + current_count += 1 + return middle_string + return f"UPDATE {self.name} SET{manufactur_update_SQL_string(data)} WHERE id = {id}" + + def convert_data(self, rows: list or tuple): + """ Takes rows returned by the tables SQL_select string and returns them as namedtuples. + + Args: + rows (listortuple): + + Returns: + (listortuple): returns a list of namedtuple. + """ + self.keys = list(self.columns.keys()) + if isinstance(rows, list): + return [self.data_object(**{key: data[i+1] for i, key in enumerate(self.keys)}) for data in rows] + if isinstance(rows, tuple): + return [self.data_object(**{key: rows[i+1] for i, key in enumerate(self.keys)})][0] + + + return Table + + +def basic_query(query: str): + """ Used as single query functions, ex. creating tables, dropping tables, updating rows + + Args: + query (str): SQL_execute string + """ + with database: + cursor = database.cursor() + cursor.execute(query) + +def create_table(table: Table, drop=False): + + if drop: + drop_table(table=table) + + try: + with database: + cursor = database.cursor() + cursor.execute(table.create_table) + except sqlite3.OperationalError: + pass + +def drop_table(table: Table): + try: + with database: + cursor = database.cursor() + cursor.execute(table.drop_table) + except sqlite3.OperationalError: + pass + + +def update_table_row_by_id(table: Table, row): + query = table.update_row_by_id(data=row[1], id=row[0]) + with database: + cursor = database.cursor() + cursor.execute(query) + + +def insert_into_table(table, data): + """ Passing a query and its data as a namedtuple will insert the query into the database. + + Args: + query (namedtuple): (query.query = SQL_execute string, query.data = tuple of column's data) + """ + query = table.insert_row(data) + assert query.query, query.data + with database: + cursor = database.cursor() + cursor.execute(query.query, query.data) + +def fetchone_from_table(table: Table, filter: tuple([str, Any]) = None, convert_data=True) -> tuple: + if filter: + query = table.select_row(column=filter[0], match=filter[1]) + else: + query = table.select_row() + + with database: + cursor = database.cursor() + cursor.execute(query) + + if not convert_data: + return cursor.fetchone() + + return table.convert_data(cursor.fetchone()) + + +def fetchall_from_table(table, filter: tuple([str, Any]) = None, convert_data=True) -> list: + """ Fetches all rows from the database using passed query + + Args: + query (str): SQL_execute string + + Returns: + list: list of rows as tuples + """ + + if filter: + query = table.select_row(column=filter[0], match=filter[1]) + else: + query = table.select_row() + + with database: + cursor = database.cursor() + cursor.execute(query) + batch = cursor.fetchall() + + if len(batch) == 1: + batch = batch[0] + + if not convert_data: + return batch + + return table.convert_data(batch) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ef74cd5 --- /dev/null +++ b/main.py @@ -0,0 +1,483 @@ +from flask import Flask, Request, render_template, request, redirect, url_for, flash, send_file, send_from_directory, Response, session, jsonify, current_app +from flask_avatars import Avatars +import os, pathlib, io +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.ext.mutable import MutableList, MutableDict +from flask_login import LoginManager, current_user, login_user, logout_user, UserMixin, login_required +from werkzeug.utils import secure_filename +import random, string, bcrypt, datetime +from copy import deepcopy +from dataclasses import dataclass +from PIL import Image +import base64, json, pathlib + +app = Flask(__name__) +avatars = Avatars(app) + +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///db.sqlite" +app.config["SECRET_KEY"] = "MYSUPERCOOLAPPKEY%#!%#!GJDAJVNEIALDOFJGNA" +app.config["UPLOAD_FOLDER"] = "media" +app.config['SECURITY_PASSWORD_HASH'] = 'bcrypt' +app.config['SECURITY_PASSWORD_SALT'] = b'$2b$12$wqKlYjmOfXPghx3FuC3Pu' +app.config['AVATARS_SAVE_PATH'] = 'static/avatars/' + +db = SQLAlchemy() + +login_manager = LoginManager() +login_manager.init_app(app) + +@dataclass +class Users(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(250), unique=True, nullable=False) + email = db.Column(db.String(250), unique=True, nullable=False) + password = db.Column(db.String(250), nullable=False) + firstname = db.Column(db.String(250), nullable=True) + lastname = db.Column(db.String(250), nullable=True) + friendcode = db.Column(db.String(250), nullable=False) + subs = db.Column(MutableList.as_mutable(db.JSON), nullable=False) + latest = db.Column(db.Integer, nullable=True) + raw_avatar = db.Column(db.String(250), nullable=False) + s_avatar = db.Column(db.String(250), nullable=False) + m_avatar = db.Column(db.String(250), nullable=False) + l_avatar = db.Column(db.String(250), nullable=False) + + +class Uploads(db.Model): + id = db.Column(db.Integer, primary_key=True) + userid = db.Column(db.Integer, nullable=False) + upload_date = db.Column(db.String(250), nullable=False) + filename = db.Column(db.String(250), nullable=False) + author = db.Column(db.String(250), nullable=False) + version = db.Column(db.String(250), nullable=False) + description = db.Column(db.String(250), nullable=True) + meta_data = db.Column(MutableDict.as_mutable(db.JSON), nullable=True) + collection_json = db.Column(MutableDict.as_mutable(db.JSON), nullable=True) + character_links = db.Column(MutableDict.as_mutable(db.JSON), nullable=True) + mod_list = db.Column(MutableList.as_mutable(db.JSON), nullable=True) + path = db.Column(db.String(250), nullable=False) + +db.init_app(app) + +with app.app_context(): + db.create_all() + +def friend_code(length): + letters = string.ascii_letters + return ''.join(random.choice(letters) for i in range(length)) + +@login_manager.user_loader +def loader_user(user_id): + return Users.query.get(user_id) + +def password_hash(input_password): + bytes = input_password.encode('utf-8') + salt = bcrypt.gensalt() + + return bcrypt.hashpw(bytes, salt) + +def check_password(input_password, hash): + bytes = input_password.encode('utf-8') + result = bcrypt.checkpw(bytes, hash) + return result + + +@app.route("/login_app/avatar", methods=["POST"]) +def login_request_avatar(): + if request.method == "POST": + username = request.json['username'] + user = Users.query.filter_by(username=username).first() + if user.s_avatar != "default": + path = f"static/{user.s_avatar}" + filename = f"{user.username}.png" + return send_file(open(path, "rb"), download_name=filename) + return Response(status=201) + +@app.route('/check_server') +def ping(): + return Response(status=200) + +@app.route('/update_app//') +def update_app(pipe, version): + current_latest = "alpha_v1.0" + if pipe == "latest": + if not current_latest == version: + update_ready = True + else: + update_ready = False + + data = { + 'latest_version': current_latest, + 'update_ready': update_ready, + 'download_url': f"/download_app/{pipe}" + } + + + return jsonify(data=data) + + return Response(status=200) + +@app.route("/upload_file", methods=["POST"]) +def save_chunks(): + if 'file' in request.files: + uploaded_file = request.files['file'] + filename = uploaded_file.filename + with open(f'{app.config['UPLOAD_FOLDER']}/{filename}', 'ab') as f: + f.write(uploaded_file.read()) + return '', 200 + return 404 + +@app.route("/upload_info", methods=['POST']) +async def upload_info(): + if not request.method == 'POST': + return Response(status=415) + + data = request.json + + user = Users.query.filter_by(username=data['username']).first() + if not check_password(data['password'], user.password): + return Response(status=415) + filename = data['filename'] + + # database upload + date = datetime.date.today() + save_path = f"{app.config['UPLOAD_FOLDER']}/{filename}" + meta_data = data['meta_data'] + collection_json = data['collection_json'] + character_links = data['character_links'] + mod_list = data['mod_list'] + print(meta_data) + upload = Uploads( + userid=user.id, + upload_date = date, + filename=filename, + author=user.username, + version="test", + description="", + meta_data = meta_data, + collection_json = collection_json, + character_links = character_links, + mod_list = mod_list, + path=save_path + ) + db.session.add(upload) + db.session.commit() + + return Response(status=200) + +@app.route("/upload_app", methods=['POST']) +async def upload_app(): + data = request.files['datas'].read() + data = json.loads(data) + if not request.method == 'POST': + return Response(status=415) + + user = Users.query.filter_by(username=data['username']).first() + if not check_password(data['password'], user.password): + return Response(status=415) + + if 'file' not in request.files: + print(request.files) + return Response(status=415) + + file = request.files['file'] + + if file.filename == '': + return Response(status=415) + filename = data['filename'] + filename = secure_filename(filename) + + # database upload + date = datetime.date.today() + save_path = f"{app.config['UPLOAD_FOLDER']}/{filename}" + meta_data = data['meta_data'] + collection_json = data['collection_json'] + character_links = data['character_links'] + mod_list = data['mods_to_copy'] + print(meta_data) + upload = Uploads( + userid=user.id, + upload_date = date, + filename=filename, + author=user.firstname, + version="test", + description="", + meta_data = meta_data, + collection_json = collection_json, + character_links = character_links, + mod_list = mod_list, + path=save_path + ) + db.session.add(upload) + db.session.commit() + + + + + # saves file + print(save_path) + file.save(save_path) + return Response(status=200) + + +@app.route("/login_app", methods=["POST"]) +def login_app(): + if request.method == "POST": + username = request.json['username'] + user = Users.query.filter_by(username=username).first() + data= { + "id": user.id, + "friendcode": user.friendcode, + "subs": user.subs, + } + if check_password(request.json['password'], user.password): + return data, 200 + return Response(status=500) + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + user = Users.query.filter_by(username=request.form.get("username")).first() + if check_password(request.form.get("password"), user.password): + login_user(user) + return redirect(url_for("home")) + return render_template("login.html") + +@app.route("/logout") +def logout(): + logout_user() + return redirect(url_for("home")) + +@app.route("/signup", methods=["GET", "POST"]) +def signup(): + if request.method == "POST": + if request.form.get('password') == request.form.get('confirm_password'): + user = Users( + username=request.form.get('username'), + email=request.form.get("email"), + password=password_hash(request.form.get("password")), + firstname=request.form.get('firstname'), + lastname=request.form.get('lastname'), + friendcode=friend_code(6), + subs=[], + raw_avatar='default', + s_avatar='default', + m_avatar='default', + l_avatar='default' + ) + db.session.add(user) + db.session.commit() + return redirect(url_for("login")) + else: + return redirect(request.url) + return render_template("signup.html") + +@app.route("/") +def home(): + return render_template("home.html") + +@app.route("/about") +def about(): + return render_template("about.html") + +was_upload = (False, "") + +@app.route("/collections/upload", methods=["POST"]) +def upload(): + if request.method == "POST": + if 'file' not in request.files: + print('no file') + flash('No file part') + return redirect(url_for('collections')) + file = request.files['file'] + if file.filename == '': + print("no selected file") + flash('No selected file') + return redirect(url_for('collections')) + if file: + filename= secure_filename(file.filename) + global was_upload + was_upload = (True, filename) + print(filename) + path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(path) + + date = datetime.date.today() + + upload = Uploads( + userid=current_user.id, + upload_date = date, + filename=filename, + author=current_user.username, + version="test", + description="", + path=path + ) + db.session.add(upload) + db.session.commit() + + return redirect(url_for('collections')) + +def generate_file_chunks(filename): + with open(filename, 'rb') as file: + while True: + chunk = file.read(4096) # Read 4096 bytes at a time + if not chunk: + break + yield chunk + + +@app.route("/download_app/latest") +def download_app(): + path = pathlib.Path(f"static/versions/latest.zip") + filename = "Collection Sharing App.zip" + return send_file(open(path, "rb"), download_name=filename) + +@app.route("/collections/download/") +def download(id): + upload = Uploads.query.filter_by(id=id).first() + abs_path = os.path.join(current_app.root_path, str(upload.path).replace('/', '\\')) + print(abs_path) + if not os.path.isfile(abs_path): + print("File not found!") + try: + with open(abs_path, "rb") as f: + print("File read success, first 10 bytes:", f.read(10)) + except Exception as e: + print("Error reading file:", e) + filename = upload.filename + return send_file(abs_path, as_attachment=True, download_name=filename) + + +@app.route("/collections/delete/", methods=["GET", "POST"]) +def delete_collection(id): + upload = Uploads.query.filter_by(id=id).first() + os.remove(upload.path) + row_to_delete = db.session.query(Uploads).filter(Uploads.id == id).one() + db.session.delete(row_to_delete) + db.session.commit() + return redirect(url_for('collections')) + + +@app.route("/collections/update/", methods=["GET", "POST"]) +def update_collection(id): + print(f"this is an update call for id:{id}") + if request.method == "POST": + print(f"this is an update call for id:{id}, request is a POST") + upload = Uploads.query.filter_by(id=id).first() + upload.author = request.form.get(f"author-{id}") + upload.version = request.form.get(f"version-{id}") + upload.description = request.form.get(f"description-{id}") + print(upload.description) + db.session.commit() + if request.form.get(f"latest-{id}") == "on": + user = Users.query.filter_by(id=current_user.id).first() + user.latest = upload.id + db.session.commit() + return redirect(url_for('collections')) + return redirect(url_for('collections')) + + +@app.route("/collections") +def collections(): + page = request.args.get('page', 1, type=int) + pagination = Uploads.query.filter_by(userid=current_user.id).order_by(Uploads.upload_date) + pagination = pagination.paginate(page=page, per_page=5, error_out=False) + data=[] + friends_data = [] + for code in current_user.subs: + try: + user = Users.query.filter_by(friendcode=code).first() + upload = Uploads.query.filter_by(id=user.latest).first() + up_data = { + "id": upload.id, + "upload_date": upload.upload_date, + "name": upload.filename, + "author": upload.author, + "version": upload.version, + "description": upload.description, + "path": pathlib.Path(f"{upload.path}").absolute() + } + friends_data.append(up_data) + except: + pass + + global was_upload + data=[pagination, friends_data, deepcopy(was_upload)] + was_upload = (False, "") + return render_template("collections.html", data=data) + +@app.route('/profile') +def profile(): + return render_template("profile.html") + +@app.route('/profile/upload', methods=["POST"]) +def upload_avatar(): + if request.method == 'POST': + f = request.files.get('file') + print(f) + raw_filename = avatars.save_avatar(f) + user = Users.query.filter_by(id=current_user.id).first() + user.raw_avatar = f"{app.config['AVATARS_SAVE_PATH']}{raw_filename}" + db.session.commit() + session['raw_filename'] = raw_filename # you will need to store this filename in database in reality + return redirect(url_for('crop')) + return redirect(url_for('profile')) + +# serve avatar image +@app.route('/avatars/') +def get_avatar(filename): + return send_from_directory(app.config['AVATARS_SAVE_PATH'], filename) + +@app.route('/crop', methods=['GET', 'POST']) +def crop(): + if request.method == 'POST': + x = request.form.get('x') + y = request.form.get('y') + w = request.form.get('w') + h = request.form.get('h') + filenames = avatars.crop_avatar(session['raw_filename'], x, y, w, h) + user = Users.query.filter_by(id=current_user.id).first() + user.s_avatar = f"avatars/{filenames[0]}" + print(user.s_avatar) + db.session.commit() + user.m_avatar = f"avatars/{filenames[1]}" + db.session.commit() + user.l_avatar = f"avatars/{filenames[2]}" + db.session.commit() + + return redirect(url_for('profile')) + return render_template('crop.html') + +@app.route('/profile/friends') +def friends_code(): + data = [] + for code in current_user.subs: + try: + user = Users.query.filter_by(friendcode=code).first() + + x = { + 'username': user.username, + 'friendcode': user.friendcode, + 'm_avatar': user.m_avatar + } + + data.append(x) + except: + pass + return render_template("friends.html", data=data) + + +@app.route("/addcode", methods=["POST"]) +def addcode(): + if request.method == "POST": + if request.form.get("friendcode") == "": + return redirect(url_for('friends_code')) + user = Users.query.filter_by(id=current_user.id).first() + user.subs.append(request.form.get("friendcode")) + user.subs = user.subs + db.session.commit() + return redirect(url_for('friends_code')) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port="5003", debug=True) \ No newline at end of file diff --git a/mysite/mysite/__init__.py b/mysite/mysite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mysite/mysite/__pycache__/__init__.cpython-312.pyc b/mysite/mysite/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..3267deb Binary files /dev/null and b/mysite/mysite/__pycache__/__init__.cpython-312.pyc differ diff --git a/mysite/mysite/__pycache__/settings.cpython-312.pyc b/mysite/mysite/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000..44acc81 Binary files /dev/null and b/mysite/mysite/__pycache__/settings.cpython-312.pyc differ diff --git a/mysite/mysite/__pycache__/urls.cpython-312.pyc b/mysite/mysite/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..9f4885f Binary files /dev/null and b/mysite/mysite/__pycache__/urls.cpython-312.pyc differ diff --git a/mysite/mysite/__pycache__/wsgi.cpython-312.pyc b/mysite/mysite/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000..a4edb6f Binary files /dev/null and b/mysite/mysite/__pycache__/wsgi.cpython-312.pyc differ diff --git a/mysite/mysite/asgi.py b/mysite/mysite/asgi.py new file mode 100644 index 0000000..cb29d22 --- /dev/null +++ b/mysite/mysite/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for mysite project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_asgi_application() diff --git a/mysite/mysite/settings.py b/mysite/mysite/settings.py new file mode 100644 index 0000000..ab26ba0 --- /dev/null +++ b/mysite/mysite/settings.py @@ -0,0 +1,124 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 5.0.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-zkb)))rp%reh1qr6ega!sn41^b(-93*)vb&u2k(h6ltk!wion9' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'catalog.apps.CatalogConfig' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/mysite/mysite/urls.py b/mysite/mysite/urls.py new file mode 100644 index 0000000..04e1793 --- /dev/null +++ b/mysite/mysite/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for mysite project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('catalog/', include('catalog.urls')), +] diff --git a/mysite/mysite/wsgi.py b/mysite/mysite/wsgi.py new file mode 100644 index 0000000..1925037 --- /dev/null +++ b/mysite/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_wsgi_application() diff --git a/saved_file.zip b/saved_file.zip new file mode 100644 index 0000000..e1ffd60 Binary files /dev/null and b/saved_file.zip differ diff --git a/static/css/template.css b/static/css/template.css new file mode 100644 index 0000000..e69de29 diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..8751d08 --- /dev/null +++ b/templates/about.html @@ -0,0 +1,24 @@ + + + + + About Flask + + + + + + + + + {% extends "template.html" %} + {% block content %} + +

About Flask

+

Flask is a micro web framework written in Python.

+

Applications that use the Flask framework include Pinterest, + LinkedIn, and the community web page for Flask itself.

+ + {% endblock %} + + \ No newline at end of file diff --git a/templates/collection_list.html b/templates/collection_list.html new file mode 100644 index 0000000..f5c4b71 --- /dev/null +++ b/templates/collection_list.html @@ -0,0 +1,111 @@ +{% block content %} +
+
+

My Collections

+ + + + + + + + + + + + {% for item in data[0].items %} + + + {% else %} + + {% endif %} + + + +
+
+ + + + {% endfor %} + +
Collection NameAuthorVersion
star{{item.filename}}{{item.filename}}{{item.author}}{{item.version}} + edit + download + delete +
+ +
+
    + {% if data[0].has_prev %} +
  • chevron_left
  • + {% endif %} + + {% for number in data[0].iter_pages() %} + {% if data[0].page != number %} +
  • {{ number }}
  • + {% else %} +
  • {{ number }}
  • + {% endif %} + {% endfor %} + + {% if data[0].has_next %} +
  • chevron_right
  • + {% endif %} + +
+
+
+
+{% endblock %} diff --git a/templates/collections.html b/templates/collections.html new file mode 100644 index 0000000..703c52a --- /dev/null +++ b/templates/collections.html @@ -0,0 +1,86 @@ + + + + + Treehouse - My Collections + + + + + + + +{% extends "template.html" %} +{% block content %} + + +
+
+
+
+
+
+
+ File + +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+ + {% include "collection_list.html" %} + +
+
+

Friends Collections

+ + + + + + + + + + + {% for item in data[1] %} + + + + + + + {% endfor %} + +
Collection NameAuthorVersion
{{item.name}}{{item.author}}{{item.version}}downloadDownload
+
+
+
+
+ +{% if data[2][0] %} + +{% endif %} +{% endblock %} + + + + \ No newline at end of file diff --git a/templates/crop.html b/templates/crop.html new file mode 100644 index 0000000..ef22b34 --- /dev/null +++ b/templates/crop.html @@ -0,0 +1,25 @@ + + + + + Flask-Avatars Demo + {{ avatars.jcrop_css() }} + + + +

Step 2: Crop

+ {{ avatars.crop_box('get_avatar', session['raw_filename']) }} + {{ avatars.preview_box('get_avatar', session['raw_filename']) }} +
+ + + + + +
+ {{ avatars.jcrop_js() }} + {{ avatars.init_jcrop() }} + + \ No newline at end of file diff --git a/templates/friends.html b/templates/friends.html new file mode 100644 index 0000000..b06cb3d --- /dev/null +++ b/templates/friends.html @@ -0,0 +1,45 @@ +{% extends "template.html" %} +{% block content %} + +
+
+
+

Friends

+
+
+
+
+
+
+ + +
+
+ +
+
+
+
    + {% for friend in data %} +
  • + + {% if friend.m_avatar == 'default' %} + + {% else %} + + {% endif %} +
    {{friend.username}}
    +
    {{friend.friendcode}}
    +
  • + {% endfor %} +
+ + +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..73fea86 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,34 @@ + + + + + Treehouse - Home + + + + {% extends "template.html" %} + {% block content %} + +
+
+
+
+
+ Collection Sharing App alpha_V1.0 +

Here is the latest version of the Collection Sharing App as of 11-23-2024. You can login to an account you set up under "sign-up" on this webpage + and upload directly to this site OR you can just export and import specific collections and share them through another means. That being said this is very much + an ALPHA app and will have its issues. +

+
+
+ Latestcloud_download +

alpha_v1.0

+
+
+
+
+
+ {% endblock %} + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..bc910a0 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,28 @@ + + + + + Signup + + + + + + + + + + {% block content %} +
+

Login to your account

+
+ + + + + +
+
+ {% endblock %} + + \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..e53a143 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,75 @@ +{% extends "template.html" %} +{% block content %} +
+
+
+
+ +
+
+ {% if current_user.l_avatar == 'default' %} + + {% else %} + + {% endif %} +
+
+

Profile

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ File + +
+
+ +
+
+
+
+
+ +
+
+
+
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..88d0f20 --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,112 @@ + + + + + Signup + + + + + + + + + + {% block content %} +
+

Signup

+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+

+
+
+
+
+ + +
+
+
+ +
+
+
+
+ {% endblock %} + + + + \ No newline at end of file diff --git a/templates/template.html b/templates/template.html new file mode 100644 index 0000000..d1cb89d --- /dev/null +++ b/templates/template.html @@ -0,0 +1,69 @@ + + + + + Treehouse - Navbar + + + + + + +
+ + + + + + +
+ + {% block content %} + + {% endblock %} + + + + + \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..4861ab4 --- /dev/null +++ b/test.py @@ -0,0 +1,36 @@ +import requests +from base64 import b64encode + +def basic_auth(username, password): + token = b64encode(f"{username}:{password}".encode('utf-8')).decode("ascii") + return f'Basic {token}' + + +requests.post("https://ntfy.treehousefullofstars.com/alerts", + data="Look ma, with auth", + headers={ + "Authorization": basic_auth('jadowyne', 'Jumbocarrot&001') + }) + +already_used = [ + [(False, None), (False, None), (True, "X")], + [(False, None), (True, "Circle"), (False, None)], + [(False, None), (False, None), (False, None)] + ] + +already_used = [(0, 0),(0, 1),(0, 2), ...] + + +spot = random(len(already_used.keys())) + +amount_x = 3 +amount_y = 3 + + +grid = [ + [], + [], + [] + ] +2, 2 +state = already_used[x][y][0] \ No newline at end of file