Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27935604fe | |||
| b4f3fa5cdb | |||
| d736ebb5bc | |||
| e9d2569c1e | |||
| 82b526b512 | |||
| c7bddeabeb | |||
| 2b835d7500 | |||
| a35085c8fd | |||
| ca0ca4741b | |||
| a0699dadc6 | |||
| 7ec14d6f85 | |||
| 5ed94e9b1a |
11 changed files with 427 additions and 114 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
data
|
||||
src/static/uploads/*
|
||||
venv
|
||||
src/static/uploads
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -1 +1,31 @@
|
|||
Minimal dating website writen in python
|
||||
# Dating-Website
|
||||
## Description
|
||||
Minimal dating website made in python.
|
||||
It uses flask to render the HTML, saves the data in a MySQL database and also features an authentication method using PGP where the database stores the user's PGP key's fingerprint and uses that to encrypt a message only you decrypt, but successfully decrypting the message you athenticate to your account.
|
||||
It's also supposed to be very easy to use, currently its still in development but the vision is that you can be enganged on the website right from the start featuring a very powerfull search page and not needing an account to use the website.
|
||||
This website also does not use JavaScript making it easy to run on any browser.
|
||||
|
||||
## TODO
|
||||
- making the website responsive
|
||||
- adding search features
|
||||
- likes and dislikes
|
||||
- cool css
|
||||
- a grid with all of the users on the index page (where the search will also be)
|
||||
- security audits
|
||||
- maybe more stuff later...
|
||||
|
||||
## Contributing
|
||||
If you have suggestions, find bugs, or want to provide code, just open an issue before submiting a PR. I will probably not accept a PR unless I see that it's actually somewhat important, exceptions can be made, but its kinda goofy to write the code before submiting an issue.
|
||||
|
||||
## Running the program
|
||||
#### Docker/Podman
|
||||
`docker compose up -d` / `podman-compose up -d`
|
||||
|
||||
### python enviornment
|
||||
`python -m venv venv`
|
||||
|
||||
### Install the dependencies
|
||||
`pip install -r requirements.txxt`
|
||||
|
||||
### Run the program
|
||||
`pyhton src/main.py`
|
||||
|
|
|
|||
131
src/main.py
131
src/main.py
|
|
@ -1,14 +1,15 @@
|
|||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import text
|
||||
from datetime import date
|
||||
import gnupg
|
||||
import secrets
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
# defines where the upload folder is and creates it
|
||||
UPLOAD_FOLDER = "static/uploads"
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # sets the base dir as the diretory where the python file is
|
||||
UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") # joins the directories
|
||||
os.makedirs(UPLOAD_FOLDER, exist_ok=True) # creates the uploads directorie
|
||||
|
||||
# configures the app
|
||||
app = Flask(__name__) # creates de app
|
||||
|
|
@ -78,26 +79,22 @@ def calculate_age(dob: date) -> int:
|
|||
|
||||
# saves files to the upload folder and returns their URL
|
||||
def save_files(username: str, profile_file, pictures_files):
|
||||
# creates a path for the user inside the upload forlder
|
||||
user_folder = os.path.join(app.config['UPLOAD_FOLDER'], username)
|
||||
os.makedirs(user_folder, exist_ok=True)
|
||||
|
||||
# prevents unsafe characters to be used in the filename
|
||||
profile_filename = secure_filename(profile_file.filename)
|
||||
profile_path = os.path.join(user_folder, profile_filename)
|
||||
|
||||
# saves the profile picture to the path
|
||||
profile_file.save(profile_path)
|
||||
profile_url = f"/{profile_path.replace(os.sep, '/')}"
|
||||
|
||||
# saves all of the other pictures
|
||||
profile_url = f"/static/uploads/{username}/{profile_filename}"
|
||||
|
||||
pictures_urls = []
|
||||
for pic in pictures_files:
|
||||
if pic.filename:
|
||||
filename = secure_filename(pic.filename)
|
||||
path = os.path.join(user_folder, filename)
|
||||
pic.save(path)
|
||||
pictures_urls.append(f"/{path.replace(os.sep, '/')}")
|
||||
pictures_urls.append(f"/static/uploads/{username}/{filename}")
|
||||
|
||||
return profile_url, pictures_urls
|
||||
|
||||
|
|
@ -119,8 +116,58 @@ def pgp_encrypt_and_import(pgp_key: str, message: str):
|
|||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return render_template("index.html")
|
||||
query = User.query
|
||||
|
||||
country = request.args.get("country")
|
||||
city = request.args.get("city")
|
||||
sex = request.args.get("sex")
|
||||
age_min = request.args.get("age_min")
|
||||
age_max = request.args.get("age_max")
|
||||
race = request.args.get("race")
|
||||
likes = request.args.get("likes")
|
||||
dislikes = request.args.get("dislikes")
|
||||
|
||||
if country:
|
||||
query = query.filter(User.country.ilike(f"%{country}%"))
|
||||
if city:
|
||||
query = query.filter(User.city.ilike(f"%{city}%"))
|
||||
if sex:
|
||||
query = query.filter(User.sex==sex)
|
||||
if race:
|
||||
query = query.filter(User.race.ilike(f"%{race}%"))
|
||||
|
||||
today = date.today()
|
||||
if age_min:
|
||||
try:
|
||||
min_age = int(age_min)
|
||||
dob_max = date(today.year - min_age, today.month, today.day)
|
||||
query = query.filter(User.date_of_birth <= dob_max)
|
||||
except ValueError:
|
||||
pass
|
||||
if age_max:
|
||||
try:
|
||||
max_age = int(age_max)
|
||||
dob_min = date(today.year - max_age - 1, today.month, today.day)
|
||||
query = query.filter(User.date_of_birth >= dob_min)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if likes:
|
||||
likes_list = [x.strip().lower() for x in likes.split(",") if x.strip()]
|
||||
for like in likes_list:
|
||||
query = query.filter(
|
||||
text(f"JSON_CONTAINS(likes, '\"{like}\"')")
|
||||
)
|
||||
|
||||
if dislikes:
|
||||
dislikes_list = [x.strip().lower() for x in dislikes.split(",") if x.strip()]
|
||||
for dislike in dislikes_list:
|
||||
query = query.filter(
|
||||
text(f"JSON_CONTAINS(dislikes, '\"{dislike}\"')")
|
||||
)
|
||||
|
||||
users = query.all()
|
||||
return render_template("index.html", users=users, date=date)
|
||||
|
||||
@app.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
|
|
@ -128,9 +175,39 @@ def register():
|
|||
# collect data to a dictionary
|
||||
data = {key: request.form.get(key) for key in [
|
||||
"username","pgp","firstname","lastname","sex","date_of_birth","country","xmpp",
|
||||
"email","phone","city","height","weight","race","prefered_age_range"
|
||||
"email","phone","city","height","weight","race"
|
||||
]}
|
||||
|
||||
min_age = request.form.get("preferred_age_min")
|
||||
max_age = request.form.get("preferred_age_max")
|
||||
|
||||
if min_age and max_age:
|
||||
try:
|
||||
min_age = int(min_age)
|
||||
max_age = int(max_age)
|
||||
|
||||
if min_age < 18 or max_age < 18:
|
||||
flash("Minimum age is 18.")
|
||||
return redirect(url_for("register"))
|
||||
|
||||
if min_age > max_age:
|
||||
flash("Minimum age cannot be greater than maximum age.")
|
||||
return redirect(url_for("register"))
|
||||
|
||||
data["prefered_age_range"] = f"{min_age}-{max_age}"
|
||||
|
||||
except ValueError:
|
||||
flash("Invalid age range.")
|
||||
return redirect(url_for("register"))
|
||||
else:
|
||||
data["prefered_age_range"] = None
|
||||
|
||||
likes_raw = request.form.get("likes", "")
|
||||
dislikes_raw = request.form.get("dislikes", "")
|
||||
|
||||
data["likes"] = list(set(x.strip().lower() for x in likes_raw.split(",") if x.strip()))
|
||||
data["dislikes"] = list(set(x.strip().lower() for x in dislikes_raw.split(",") if x.strip()))
|
||||
|
||||
# required fields
|
||||
required_fields = ["username","pgp","firstname","lastname","sex","date_of_birth","country","xmpp"]
|
||||
if not all(data[f] for f in required_fields):
|
||||
|
|
@ -169,19 +246,30 @@ def register():
|
|||
|
||||
# creates a random string
|
||||
random_string = secrets.token_hex(16)
|
||||
|
||||
# uses the string to create the message that wll be encrypted
|
||||
challenge_phrase = f"this is the unencrypted string: {random_string}"
|
||||
|
||||
# encrypts message
|
||||
fingerprint, encrypted_msg = pgp_encrypt_and_import(data["pgp"], challenge_phrase)
|
||||
|
||||
|
||||
|
||||
# checks fingerprint
|
||||
if not fingerprint or not encrypted_msg:
|
||||
flash("Invalid PGP key or encryption failed.")
|
||||
return redirect(url_for("register"))
|
||||
print(fingerprint)
|
||||
|
||||
# creates a temporary session used to verify the user
|
||||
session["pending_user"] = {**data, "profile_url": profile_url, "pictures_urls": pictures_urls}
|
||||
session["pgp_expected_phrase"] = challenge_phrase
|
||||
session["pending_user"] = {
|
||||
**data,
|
||||
"profile_url": profile_url,
|
||||
"pictures_urls": pictures_urls,
|
||||
"fingerprint": fingerprint
|
||||
}
|
||||
|
||||
session['pgp_expected_phrase'] = challenge_phrase
|
||||
|
||||
# renders the verification page
|
||||
return render_template("verify.html", encrypted_message=encrypted_msg)
|
||||
|
|
@ -191,18 +279,23 @@ def register():
|
|||
|
||||
@app.route("/verify", methods=["POST"])
|
||||
def verify():
|
||||
# retrieve the phrase from the session
|
||||
expected_phrase = session.get("pgp_expected_phrase")
|
||||
# retrieve user data from the session
|
||||
data = session.get("pending_user")
|
||||
|
||||
fingerprint = data.get("fingerprint")
|
||||
|
||||
# retrieve the phrase from the session
|
||||
expected_phrase = session.get("pgp_expected_phrase")
|
||||
|
||||
|
||||
# check to see if data exists
|
||||
if not data or not expected_phrase:
|
||||
flash("Session expired.")
|
||||
return redirect(url_for("register"))
|
||||
|
||||
# get the decrypted message
|
||||
# get the decrypted message from form
|
||||
submitted = request.form.get("decrypted_message")
|
||||
|
||||
# check to see if submission was empty
|
||||
if not submitted:
|
||||
flash("You must paste the decrypted message.")
|
||||
|
|
@ -234,6 +327,8 @@ def verify():
|
|||
height=float(data["height"]) if data.get("height") else None,
|
||||
weight=int(data["weight"]) if data.get("weight") else None,
|
||||
race=data.get("race") or None,
|
||||
likes=data.get("likes") or [],
|
||||
dislikes=data.get("dislikes") or [],
|
||||
prefered_age_range=data.get("prefered_age_range") or None,
|
||||
is_verified=True
|
||||
)
|
||||
|
|
@ -244,9 +339,9 @@ def verify():
|
|||
# creates login session
|
||||
session['user_id'] = new_user.id
|
||||
session['username'] = new_user.username
|
||||
|
||||
# remove temporary session
|
||||
session.pop("pending_user", None)
|
||||
session.pop("pgp_expected_phrase", None)
|
||||
|
||||
flash("PGP verification successful! Account created.")
|
||||
return redirect(url_for("home"))
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
@font-face {
|
||||
font-family: 'font';
|
||||
src: url('/font/font.ttf') format('truetype');
|
||||
src: url('/static/font/font.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'font';
|
||||
src: url('/font/font-Bold.ttf') format('truetype');
|
||||
src: url('/static/font/font-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #101210;
|
||||
color: #e0e0e0;
|
||||
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.8);
|
||||
max-width: 800px;
|
||||
background: #FFE0F4;
|
||||
color: #FF00AA;
|
||||
text-shadow: 0px 0px 5px rgba(255, 0, 170, 0.8);
|
||||
padding: 5px;
|
||||
max-width: 75%;
|
||||
margin: auto;
|
||||
font-family: font;
|
||||
font-weight: normal;
|
||||
line-height: 1.2rem;
|
||||
word-wrap: break-word;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
|
@ -42,44 +44,55 @@ strong, b {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-top: 32px;
|
||||
background: #fff;
|
||||
padding: 5px;
|
||||
border: medium;
|
||||
border-color: #FF00AA;
|
||||
border-radius: 5px;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
color: #00ff00;
|
||||
color: #FF00AA;
|
||||
text-decoration: underline yellow;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #00ff00;
|
||||
color: #FF00AA;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #00ff00;
|
||||
color: #FF00AA;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ffff00;
|
||||
color: #FF699B;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
summary {
|
||||
color: #008800;
|
||||
background: #101210;
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
details {
|
||||
background: #222;
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
summary:hover {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
th {
|
||||
background-color: #FF00AA;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.service {
|
||||
padding: 0.5rem;
|
||||
border: solid thin #ffff00;
|
||||
tr:nth-child(even) {
|
||||
background-color: #FF00AA;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,47 @@
|
|||
{% extends "page.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Home</h2>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<p>Page text</p>
|
||||
<h2>Discover Users</h2>
|
||||
|
||||
<section>
|
||||
<h3>Search Users</h3>
|
||||
<form method="GET" action="{{ url_for('home') }}">
|
||||
<input type="text" name="country" placeholder="Country" value="{{ request.args.get('country', '') }}">
|
||||
<input type="text" name="city" placeholder="City" value="{{ request.args.get('city', '') }}">
|
||||
|
||||
<select name="sex">
|
||||
<option value="">Any Sex</option>
|
||||
<option value="male" {% if request.args.get('sex')=='male' %}selected{% endif %}>Male</option>
|
||||
<option value="female" {% if request.args.get('sex')=='female' %}selected{% endif %}>Female</option>
|
||||
</select>
|
||||
|
||||
<input type="number" name="age_min" placeholder="Min Age" min="18" value="{{ request.args.get('age_min', '') }}">
|
||||
<input type="number" name="age_max" placeholder="Max Age" min="18" value="{{ request.args.get('age_max', '') }}">
|
||||
|
||||
<input type="text" name="race" placeholder="Race" value="{{ request.args.get('race', '') }}">
|
||||
<input type="text" name="likes" placeholder="Likes" value="{{ request.args.get('likes', '') }}">
|
||||
<input type="text" name="dislikes" placeholder="Dislikes" value="{{ request.args.get('dislikes', '') }}">
|
||||
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Users</h3>
|
||||
{% if users %}
|
||||
<div>
|
||||
{% for user in users %}
|
||||
<div>
|
||||
<a href="{{ url_for('user_profile', username=user.username) }}">
|
||||
<img src="{{ user.profile_picture }}" alt="{{ user.username }}" width="100"><br>
|
||||
Age: {{ (date.today() - user.date_of_birth).days // 365 }}<br>
|
||||
Country: {{ user.country }}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No users found.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,34 @@
|
|||
|
||||
{% block content %}
|
||||
<h2>Login</h2>
|
||||
<p>Enter your username and PGP public key to receive a challenge.</p>
|
||||
<p>Enter your username and PGP public key to receive a verification challenge.</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('login') }}">
|
||||
<label>Username:</label><br>
|
||||
<input type="text" name="username" required><br><br>
|
||||
|
||||
<label>PGP Public Key:</label><br>
|
||||
<textarea name="pgp" rows="8" cols="60" required></textarea><br><br>
|
||||
<fieldset>
|
||||
<legend>Account Verification</legend>
|
||||
|
||||
<button type="submit">Send Challenge</button>
|
||||
<label for="username">Username</label><br>
|
||||
<input type="text" id="username" name="username" required><br><br>
|
||||
|
||||
<label for="pgp">PGP Public Key</label><br>
|
||||
<textarea id="pgp" name="pgp" rows="8" required></textarea><br>
|
||||
<small>Paste your full public key block</small>
|
||||
</fieldset>
|
||||
|
||||
<br>
|
||||
<button type="submit">Send Challenge</button>
|
||||
</form>
|
||||
|
||||
<!-- Flash messages -->
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul>
|
||||
{% for message in messages %}
|
||||
<li style="color:red;">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,7 @@
|
|||
</nav>
|
||||
|
||||
<body>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
<footer>Dating Website</footer>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -5,52 +5,117 @@
|
|||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
|
||||
<h3>Identity (required)</h3>
|
||||
<input type="text" name="username" placeholder="Username" required><br>
|
||||
<textarea name="pgp" placeholder="PGP Public Key" required></textarea><br>
|
||||
<fieldset>
|
||||
<legend>Account (required)</legend>
|
||||
|
||||
<h3>Personal Info (required)</h3>
|
||||
<input type="text" name="firstname" placeholder="First Name" required><br>
|
||||
<input type="text" name="lastname" placeholder="Last Name" required><br>
|
||||
<label for="username">Username</label><br>
|
||||
<input type="text" id="username" name="username" required><br><br>
|
||||
|
||||
<select name="sex" required>
|
||||
<label for="pgp">PGP Public Key</label><br>
|
||||
<textarea id="pgp" name="pgp" rows="6" required></textarea>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Personal Info (required)</legend>
|
||||
|
||||
<label for="firstname">First Name</label><br>
|
||||
<input type="text" id="firstname" name="firstname" required><br><br>
|
||||
|
||||
<label for="lastname">Last Name</label><br>
|
||||
<input type="text" id="lastname" name="lastname" required><br><br>
|
||||
|
||||
<label for="sex">Sex</label><br>
|
||||
<select id="sex" name="sex" required>
|
||||
<option value="">Select Sex</option>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
</select><br>
|
||||
</select><br><br>
|
||||
|
||||
<input type="date" name="date_of_birth" required><br>
|
||||
<label for="date_of_birth">Date of Birth</label><br>
|
||||
<input type="date" id="date_of_birth" name="date_of_birth" required>
|
||||
</fieldset>
|
||||
|
||||
<h3>Profile Picture (required)</h3>
|
||||
<input type="file" name="profile_picture" accept="image/*" required><br>
|
||||
<fieldset>
|
||||
<legend>Pictures</legend>
|
||||
|
||||
<h3>Other Pictures (optional, multiple)</h3>
|
||||
<input type="file" name="pictures" accept="image/*" multiple><br>
|
||||
<label for="profile_picture">Profile Picture (required)</label><br>
|
||||
<input type="file" id="profile_picture" name="profile_picture" accept="image/*" required><br><br>
|
||||
|
||||
<h3>Location (required)</h3>
|
||||
<select name="country" required>
|
||||
<label for="pictures">Other Pictures (optional)</label><br>
|
||||
<input type="file" id="pictures" name="pictures" accept="image/*" multiple>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Location</legend>
|
||||
|
||||
<label for="country">Country</label><br>
|
||||
<select id="country" name="country" required>
|
||||
<option value="">Select Country</option>
|
||||
{% for c in countries %}
|
||||
<option value="{{ c }}">{{ c }}</option>
|
||||
{% endfor %}
|
||||
</select><br>
|
||||
<input type="text" name="city" placeholder="City"><br>
|
||||
</select><br><br>
|
||||
|
||||
<h3>Physical Attributes (optional)</h3>
|
||||
<input type="number" step="0.01" name="height" placeholder="Height (m)"><br>
|
||||
<input type="number" name="weight" placeholder="Weight (kg)"><br>
|
||||
<input type="text" name="race" placeholder="Race"><br>
|
||||
<label for="city">City</label><br>
|
||||
<input type="text" id="city" name="city">
|
||||
</fieldset>
|
||||
|
||||
<h3>Preferences (optional)</h3>
|
||||
<input type="text" name="prefered_age_range" placeholder="Preferred Age Range (e.g. 20-30)"><br>
|
||||
<fieldset>
|
||||
<legend>Physical Attributes</legend>
|
||||
|
||||
<h3>Contacts (required)</h3>
|
||||
<input type="text" name="xmpp" placeholder="XMPP" required><br>
|
||||
<input type="email" name="email" placeholder="Email"><br>
|
||||
<input type="text" name="phone" placeholder="Phone"><br><br>
|
||||
<label for="height">Height (meters)</label><br>
|
||||
<input type="number" step="0.01" id="height" name="height"><br><br>
|
||||
|
||||
<button type="submit">Register</button>
|
||||
<label for="weight">Weight (kg)</label><br>
|
||||
<input type="number" id="weight" name="weight"><br><br>
|
||||
|
||||
<label for="race">Race</label><br>
|
||||
<select id="race" name="race">
|
||||
<option value="">Select race</option>
|
||||
<option value="Latino(a)">Latino</option>
|
||||
<option value="Asian">Asian</option>
|
||||
<option value="White">White</option>
|
||||
<option value="Black">Black</option>
|
||||
<option value="Brown">Brown</option>
|
||||
<option value="Native American">Native American</option>
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Preferences</legend>
|
||||
|
||||
<label>Preferred Age Range</label><br>
|
||||
|
||||
<label for="preferred_age_min">Minimum Age</label><br>
|
||||
<input type="number" id="preferred_age_min" name="preferred_age_min" min="18" max="120"><br>
|
||||
|
||||
<label for="preferred_age_max">Maximum Age</label><br>
|
||||
<input type="number" id="preferred_age_max" name="preferred_age_max" min="18" max="120"><br>
|
||||
|
||||
<label for="likes">Things You Like</label><br>
|
||||
<input type="text" id="likes" name="likes" placeholder="music, travel, coding"><br>
|
||||
<small>Separate with commas</small><br><br>
|
||||
|
||||
<label for="dislikes">Things You Don't Like</label><br>
|
||||
<input type="text" id="dislikes" name="dislikes" placeholder="smoking, drama"><br>
|
||||
<small>Separate with commas</small>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Contacts (required)</legend>
|
||||
|
||||
<label for="xmpp">XMPP</label><br>
|
||||
<input type="text" id="xmpp" name="xmpp" required><br><br>
|
||||
|
||||
<label for="email">Email</label><br>
|
||||
<input type="email" id="email" name="email"><br><br>
|
||||
|
||||
<label for="phone">Phone</label><br>
|
||||
<input type="text" id="phone" name="phone">
|
||||
</fieldset>
|
||||
|
||||
<br>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,97 @@
|
|||
{% extends "page.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ user.firstname }} {{ user.lastname }}</h2>
|
||||
<img src="{{ user.profile_picture }}" alt="Profile Picture" width="150"><br>
|
||||
|
||||
<section>
|
||||
<h2>{{ user.firstname }} {{ user.lastname }}</h2>
|
||||
|
||||
<img src="{{ user.profile_picture }}" alt="Profile Picture" style="max-width:200px; border-radius:8px;"><br><br>
|
||||
|
||||
<p><strong>Age:</strong> {{ (date.today() - user.date_of_birth).days // 365 }}</p>
|
||||
<p><strong>Sex:</strong> {{ user.sex|capitalize }}</p>
|
||||
</section>
|
||||
|
||||
|
||||
{% if user.pictures %}
|
||||
<h3>Pictures</h3>
|
||||
{% for pic in user.pictures %}
|
||||
<img src="{{ pic }}" width="100">
|
||||
{% endfor %}
|
||||
<section>
|
||||
<h3>Gallery</h3>
|
||||
{% for pic in user.pictures %}
|
||||
<img src="{{ pic }}" style="max-width:120px; margin:5px; border-radius:6px;">
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<h3>Personal Info</h3>
|
||||
<p>Sex: {{ user.sex }}</p>
|
||||
<p>Date of Birth: {{ user.date_of_birth }}</p>
|
||||
<p>Age: {{ (date.today() - user.date_of_birth).days // 365 }}</p>
|
||||
<p>Race: {{ user.race or 'Not specified' }}</p>
|
||||
|
||||
<h3>Location</h3>
|
||||
<p>Country: {{ user.country }}</p>
|
||||
<p>City: {{ user.city or 'Not specified' }}</p>
|
||||
<section>
|
||||
<h3>Personal Info</h3>
|
||||
<p><strong>Date of Birth:</strong> {{ user.date_of_birth }}</p>
|
||||
<p><strong>Race:</strong> {{ user.race or 'Not specified' }}</p>
|
||||
</section>
|
||||
|
||||
<h3>Physical Attributes</h3>
|
||||
<p>Height: {{ user.height or 'Not specified' }} m</p>
|
||||
<p>Weight: {{ user.weight or 'Not specified' }} kg</p>
|
||||
|
||||
<h3>Preferences</h3>
|
||||
<p>Preferred Age Range: {{ user.prefered_age_range or 'Not specified' }}</p>
|
||||
<p>Likes: {{ user.likes | join(', ') if user.likes else 'Not specified' }}</p>
|
||||
<p>Dislikes: {{ user.dislikes | join(', ') if user.dislikes else 'Not specified' }}</p>
|
||||
<section>
|
||||
<h3>Location</h3>
|
||||
<p><strong>Country:</strong> {{ user.country }}</p>
|
||||
<p><strong>City:</strong> {{ user.city or 'Not specified' }}</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<h3>Physical Attributes</h3>
|
||||
<p><strong>Height:</strong>
|
||||
{% if user.height %}
|
||||
{{ user.height }} m
|
||||
{% else %}
|
||||
Not specified
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<p><strong>Weight:</strong>
|
||||
{% if user.weight %}
|
||||
{{ user.weight }} kg
|
||||
{% else %}
|
||||
Not specified
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<h3>Preferences</h3>
|
||||
|
||||
<p><strong>Preferred Age Range:</strong>
|
||||
{{ user.prefered_age_range or 'Not specified' }}
|
||||
</p>
|
||||
|
||||
<p><strong>Likes:</strong></p>
|
||||
{% if user.likes %}
|
||||
<ul>
|
||||
{% for like in user.likes %}
|
||||
<li>{{ like }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Not specified</p>
|
||||
{% endif %}
|
||||
|
||||
<p><strong>Dislikes:</strong></p>
|
||||
{% if user.dislikes %}
|
||||
<ul>
|
||||
{% for dislike in user.dislikes %}
|
||||
<li>{{ dislike }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Not specified</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
|
||||
<section>
|
||||
<h3>Contacts</h3>
|
||||
<p><strong>XMPP:</strong> {{ user.xmpp }}</p>
|
||||
<p><strong>Email:</strong> {{ user.email or 'Not specified' }}</p>
|
||||
<p><strong>Phone:</strong> {{ user.phone or 'Not specified' }}</p>
|
||||
</section>
|
||||
|
||||
<h3>Contacts</h3>
|
||||
<p>XMPP: {{ user.xmpp }}</p>
|
||||
<p>Email: {{ user.email or 'Not specified' }}</p>
|
||||
<p>Phone: {{ user.phone or 'Not specified' }}</p>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue