Skip to content

Commit 62e2dd5

Browse files
authored
Merge pull request #150 from Seluj78/tags
2 parents 7c91fe0 + 03bbb13 commit 62e2dd5

File tree

9 files changed

+1030
-46
lines changed

9 files changed

+1030
-46
lines changed

PyMatcha.postman_collection.json

Lines changed: 740 additions & 24 deletions
Large diffs are not rendered by default.

backend/PyMatcha/models/tag.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
PyMatcha - A Python Dating Website
3+
Copyright (C) 2018-2019 jlasne/gmorer
4+
<jlasne@student.42.fr> - <gmorer@student.42.fr>
5+
6+
This program is free software: you can redistribute it and/or modify
7+
it under the terms of the GNU General Public License as published by
8+
the Free Software Foundation, either version 3 of the License, or
9+
(at your option) any later version.
10+
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
16+
You should have received a copy of the GNU General Public License
17+
along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
"""
19+
from __future__ import annotations
20+
21+
import logging
22+
from typing import Dict
23+
24+
from PyMatcha.utils import create_tags_table
25+
from PyMatcha.utils.orm import Field
26+
from PyMatcha.utils.orm import Model
27+
28+
29+
class Tag(Model):
30+
table_name = "tags"
31+
32+
id = Field(int, modifiable=False)
33+
user_id = Field(int)
34+
name = Field(str)
35+
36+
def before_init(self, data):
37+
pass
38+
39+
@staticmethod
40+
def create(user_id: int, name="") -> Tag:
41+
new_tag = Tag(user_id=user_id, name=name)
42+
new_tag.save()
43+
logging.debug("Creating new tag")
44+
return new_tag
45+
46+
def get_all_info(self) -> Dict:
47+
return {"id": self.id, "user_id": self.user_id, "name": self.name}
48+
49+
@classmethod
50+
def create_table(cls):
51+
create_tags_table(cls.db)

backend/PyMatcha/models/user.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import datetime
2222
import hashlib
23-
import json
2423
import logging
2524
from typing import Any
2625
from typing import Dict
@@ -31,6 +30,7 @@
3130
import PyMatcha.models.user_image as user_image
3231
from PyMatcha.errors import ConflictError
3332
from PyMatcha.errors import NotFoundError
33+
from PyMatcha.models.tag import Tag
3434
from PyMatcha.utils import create_user_table
3535
from PyMatcha.utils import hash_password
3636
from PyMatcha.utils.orm import Field
@@ -53,7 +53,6 @@ class User(Model):
5353
orientation = Field(str)
5454
birthdate = Field(datetime.date)
5555
geohash = Field(str)
56-
tags = Field(dict)
5756
heat_score = Field(int)
5857
is_online = Field(bool)
5958
date_joined = Field(datetime.datetime, fmt="%Y-%m-%d %H:%M:%S")
@@ -86,7 +85,6 @@ def create(
8685
orientation: str,
8786
birthdate: datetime.date,
8887
geohash: str,
89-
tags,
9088
heat_score: int = 0,
9189
is_online: bool = False,
9290
date_joined: datetime.datetime = datetime.datetime.utcnow(),
@@ -135,8 +133,6 @@ def create(
135133
logging.error("Geohash error: {}".format(e))
136134
raise e
137135

138-
# TODO: Check if all tags are set in tags
139-
140136
# Encrypt password
141137
password = hash_password(password)
142138

@@ -151,7 +147,6 @@ def create(
151147
orientation=orientation,
152148
birthdate=birthdate,
153149
geohash=geohash,
154-
tags=str(json.dumps(tags)),
155150
heat_score=heat_score,
156151
is_online=is_online,
157152
date_joined=date_joined,
@@ -199,7 +194,6 @@ def register(email: str, username: str, password: str, first_name: str, last_nam
199194
orientation="bisexual",
200195
birthdate=None,
201196
geohash=None,
202-
tags="",
203197
heat_score=0,
204198
is_online=False,
205199
date_joined=datetime.datetime.utcnow(),
@@ -220,21 +214,19 @@ def get_all_info(self) -> Dict:
220214
"last_name": self.last_name,
221215
"email": self.email,
222216
"username": self.username,
223-
"password": self.password,
224217
"bio": self.bio,
225218
"gender": self.gender,
226219
"orientation": self.orientation,
227220
"birthdate": self.birthdate,
228221
"geohash": self.geohash,
229-
"tags": json.loads(self.tags) if self.tags else "", # TODO: Optimize this
222+
"tags": [t.name for t in self.get_tags()],
230223
"heat_score": self.heat_score,
231224
"is_online": self.is_online,
232225
"date_joined": self.date_joined,
233226
"date_lastseen": self.date_lastseen,
234227
"is_profile_completed": self.is_profile_completed,
235228
"is_confirmed": self.is_confirmed,
236229
"confirmed_on": self.confirmed_on,
237-
"previous_reset_token": self.previous_reset_token,
238230
}
239231

240232
@classmethod
@@ -250,7 +242,7 @@ def get_images(self) -> List[UserImage]:
250242
user_images.timestamp as timestamp, user_images.is_primary as is_primary
251243
FROM users
252244
INNER JOIN user_images on users.id = user_images.user_id
253-
WHERE users.id = CAST({} AS INT)
245+
WHERE users.id = CAST({} AS UNSIGNED)
254246
""".format(
255247
self.id
256248
)
@@ -270,6 +262,25 @@ def get_base_info(self):
270262
"date_lastseen": self.date_lastseen,
271263
}
272264

265+
def get_tags(self):
266+
logging.debug("Getting all tags for user {}".format(self.id))
267+
with self.db.cursor() as c:
268+
c.execute(
269+
"""
270+
SELECT tags.id as id, tags.user_id as user_id, tags.name as name
271+
FROM users
272+
INNER JOIN tags on users.id = tags.user_id
273+
WHERE users.id = CAST({} AS UNSIGNED)
274+
""".format(
275+
self.id
276+
)
277+
)
278+
tags = c.fetchall()
279+
tags_list = []
280+
for t in tags:
281+
tags_list.append(Tag(t))
282+
return tags_list
283+
273284

274285
def get_user(uid: Any[int, str]) -> Optional[User]:
275286
not_found = 0

backend/PyMatcha/routes/api/profile.py

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,40 @@
1616
You should have received a copy of the GNU General Public License
1717
along with this program. If not, see <https://www.gnu.org/licenses/>.
1818
"""
19+
import datetime
20+
import os
21+
1922
import flask_jwt_extended as fjwt
23+
import Geohash
2024
import PyMatcha.models.user as user
2125
from flask import Blueprint
2226
from flask import request
27+
from ip2geotools.databases.noncommercial import DbIpCity
2328
from PyMatcha.errors import BadRequestError
29+
from PyMatcha.errors import NotFoundError
30+
from PyMatcha.errors import UnauthorizedError
31+
from PyMatcha.models.tag import Tag
2432
from PyMatcha.success import Success
33+
from PyMatcha.utils import hash_password
34+
from PyMatcha.utils.confirm_token import generate_confirmation_token
2535
from PyMatcha.utils.decorators import validate_params
36+
from PyMatcha.utils.mail import send_mail_text
2637

2738
User = user.User
2839
get_user = user.get_user
2940

3041
profile_bp = Blueprint("profile", __name__)
3142

32-
REQUIRED_PARAMS_COMPLETE_PROFILE = {
33-
"orientation": str,
43+
REQUIRED_PARAMS_COMPLETE_PROFILE = {"gender": str, "birthdate": int, "orientation": str, "bio": str, "tags": list}
44+
REQUIRED_PARAMS_EDIT_PROFILE = {
45+
"first_name": str,
46+
"last_name": str,
47+
"username": str,
3448
"bio": str,
35-
# "tags": str
49+
"gender": str,
50+
"orientation": str,
51+
"birthdate": int,
52+
"tags": list,
3653
}
3754

3855

@@ -48,12 +65,127 @@ def complete_profile():
4865
data = request.get_json()
4966
orientation = data["orientation"]
5067
bio = data["bio"]
68+
tags = data["tags"]
69+
gender = data["gender"]
70+
birthdate = data["birthdate"]
5171

52-
if orientation not in ["heterosexual", "homosexual", "bisexual", "other"]:
53-
raise BadRequestError("Genre must be heterosexual, homosexual, bisexual or other", "Try again")
72+
for tag in tags:
73+
Tag.create(name=tag, user_id=current_user.id)
5474

5575
current_user.orientation = orientation
5676
current_user.bio = bio
5777
current_user.is_profile_completed = True
78+
current_user.gender = gender
79+
current_user.birthdate = datetime.date.fromtimestamp(int(birthdate))
5880
current_user.save()
5981
return Success("Profile completed !")
82+
83+
84+
@profile_bp.route("/profile/edit", methods=["PUT"])
85+
@fjwt.jwt_required
86+
@validate_params(REQUIRED_PARAMS_EDIT_PROFILE)
87+
def edit_profile():
88+
current_user = fjwt.current_user
89+
if not current_user.is_profile_completed:
90+
raise BadRequestError("The user has not completed his profile", "Complete your profile and try again")
91+
data = request.get_json()
92+
first_name = data["first_name"]
93+
last_name = data["last_name"]
94+
username = data["username"]
95+
bio = data["bio"]
96+
gender = data["gender"]
97+
orientation = data["orientation"]
98+
birthdate = data["birthdate"]
99+
100+
try:
101+
get_user(username)
102+
except NotFoundError:
103+
pass
104+
else:
105+
raise BadRequestError("Username taken", "Try again")
106+
107+
if orientation not in ["heterosexual", "homosexual", "bisexual", "other"]:
108+
raise BadRequestError("Orientation must be heterosexual, homosexual, bisexual or other", "Try again")
109+
110+
if gender not in ["male", "female", "other"]:
111+
raise BadRequestError("Gender must be male, female or other", "Try again")
112+
113+
birthdate = datetime.date.fromtimestamp(birthdate)
114+
115+
current_user.first_name = first_name
116+
current_user.last_name = last_name
117+
current_user.username = username
118+
current_user.bio = bio
119+
current_user.gender = gender
120+
current_user.orientation = orientation
121+
current_user.birthdate = birthdate
122+
current_user.save()
123+
return Success("User successfully modified !")
124+
125+
126+
@profile_bp.route("/profile/edit/email", methods=["PUT"])
127+
@fjwt.jwt_required
128+
@validate_params({"email": str})
129+
def edit_email():
130+
data = request.get_json()
131+
new_email = data["email"].lower()
132+
current_user = fjwt.current_user
133+
if current_user.email == new_email:
134+
raise BadRequestError("The new email is the same as the old one !", "Try again")
135+
current_user.email = new_email
136+
current_user.is_confirmed = False
137+
current_user.save()
138+
token = generate_confirmation_token(email=new_email, token_type="confirm")
139+
send_mail_text.delay(
140+
dest=data["email"],
141+
subject="Confirm your new email for PyMatcha",
142+
body=os.getenv("APP_URL") + "/auth/confirm/" + token,
143+
)
144+
return Success("Email sent for new email")
145+
146+
147+
@profile_bp.route("/profile/edit/password", methods=["PUT"])
148+
@fjwt.jwt_required
149+
@validate_params({"old_password": str, "new_password": str})
150+
def edit_password():
151+
data = request.get_json()
152+
old_password = data["old_password"]
153+
new_password = data["new_password"]
154+
current_user = fjwt.current_user
155+
if not current_user.check_password(old_password):
156+
raise UnauthorizedError("Incorrect password", "Try again")
157+
current_user.password = hash_password(new_password)
158+
current_user.save()
159+
# TODO: Send mail
160+
send_mail_text.delay(
161+
dest=current_user.email,
162+
subject="Password change notification",
163+
body=f"Your password was changed at {datetime.datetime.utcnow()}."
164+
f" If you believe it wasn't you, please change it immediatly",
165+
)
166+
return Success("User password successfully updated.")
167+
168+
169+
@profile_bp.route("/profile/edit/geolocation", methods=["PUT"])
170+
@fjwt.jwt_required
171+
@validate_params({"ip": str}, {"lat": float, "lng": float})
172+
def edit_geolocation():
173+
data = request.get_json()
174+
ip = data["ip"]
175+
current_user = fjwt.current_user
176+
try:
177+
lat = data["lat"]
178+
lng = data["lng"]
179+
except KeyError:
180+
lat = None
181+
lng = None
182+
183+
if lat and lng:
184+
current_user.geohash = Geohash.encode(lat, lng)
185+
else:
186+
response = DbIpCity.get(ip, api_key="free")
187+
lat = response.latitude
188+
lng = response.longitude
189+
current_user.geohash = Geohash.encode(lat, lng)
190+
current_user.save()
191+
return Success("New location sucessfully saved.")

backend/PyMatcha/utils/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from PyMatcha.utils.password import hash_password
2-
from PyMatcha.utils.tables import create_tables, _create_user_images_table, _create_user_table
2+
from PyMatcha.utils.tables import _create_tags_table
3+
from PyMatcha.utils.tables import _create_user_images_table
4+
from PyMatcha.utils.tables import _create_user_table
5+
from PyMatcha.utils.tables import create_tables
36

47
create_user_images_table = _create_user_images_table
58
create_user_table = _create_user_table
9+
create_tags_table = _create_tags_table
610

7-
__all__ = ["hash_password", "create_tables", "create_user_table", "create_user_images_table"]
11+
__all__ = ["hash_password", "create_tables", "create_user_table", "create_user_images_table", "create_tags_table"]

backend/PyMatcha/utils/tables.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ def _create_user_table(db):
3838
orientation ENUM('heterosexual', 'homosexual', 'bisexual', 'other'),
3939
birthdate DATE DEFAULT NULL,
4040
geohash VARCHAR(256) DEFAULT NULL,
41-
tags LONGTEXT, # TODO: JSON ?
4241
heat_score INT DEFAULT (0),
4342
date_joined DATETIME DEFAULT NOW(),
4443
date_lastseen DATETIME DEFAULT NOW(),
@@ -72,6 +71,24 @@ def _create_user_images_table(db):
7271
c.execute("""SET sql_notes = 1;""")
7372

7473

74+
def _create_tags_table(db):
75+
with db.cursor() as c:
76+
logging.info("Creating table tags.")
77+
c.execute("""SET sql_notes = 0;""")
78+
c.execute(
79+
"""
80+
CREATE TABLE IF NOT EXISTS tags
81+
(
82+
id INT auto_increment PRIMARY KEY,
83+
user_id INT NOT NULL,
84+
name VARCHAR(256)
85+
)
86+
"""
87+
)
88+
c.execute("""SET sql_notes = 1;""")
89+
90+
7591
def create_tables(db):
7692
_create_user_table(db)
7793
_create_user_images_table(db)
94+
_create_tags_table(db)

0 commit comments

Comments
 (0)