Skip to content

Commit 8a486c3

Browse files
authored
Merge pull request piccolo-orm#97 from piccolo-orm/user_model_enhancements
User model enhancements
2 parents ff45e3e + e6207fb commit 8a486c3

File tree

10 files changed

+207
-23
lines changed

10 files changed

+207
-23
lines changed

piccolo/apps/migrations/commands/check.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1+
import dataclasses
2+
import typing as t
3+
14
from piccolo.apps.migrations.commands.base import BaseMigrationManager
25
from piccolo.apps.migrations.tables import Migration
36
from piccolo.utils.printing import get_fixed_length_string
47

58

9+
@dataclasses.dataclass
10+
class MigrationStatus:
11+
app_name: str
12+
migration_id: str
13+
has_ran: bool
14+
15+
616
class CheckMigrationManager(BaseMigrationManager):
717
def __init__(self, app_name: str):
818
self.app_name = app_name
919
super().__init__()
1020

11-
async def run(self):
12-
print("Listing migrations ...")
13-
21+
async def get_migration_statuses(self) -> t.List[MigrationStatus]:
1422
# Make sure the migration table exists, otherwise we'll get an error.
1523
await self.create_migration_table()
1624

17-
print(
18-
f'{get_fixed_length_string("APP NAME")} | '
19-
f'{get_fixed_length_string("MIGRATION_ID")} | RAN'
20-
)
25+
migration_statuses: t.List[MigrationStatus] = []
2126

2227
app_modules = self.get_app_modules()
2328

@@ -29,8 +34,6 @@ async def run(self):
2934
if (self.app_name != "all") and (self.app_name != app_name):
3035
continue
3136

32-
fixed_length_app_name = get_fixed_length_string(app_name)
33-
3437
migration_modules = self.get_migration_modules(
3538
app_config.migrations_folder_path
3639
)
@@ -44,11 +47,53 @@ async def run(self):
4447
)
4548
.run()
4649
)
47-
fixed_length_id = get_fixed_length_string(_id)
48-
print(
49-
f"{fixed_length_app_name} | {fixed_length_id} | {has_ran}"
50+
migration_statuses.append(
51+
MigrationStatus(
52+
app_name=app_name, migration_id=_id, has_ran=has_ran
53+
)
5054
)
5155

56+
return migration_statuses
57+
58+
async def have_ran_count(self) -> int:
59+
"""
60+
:returns:
61+
The number of migrations which have been ran.
62+
"""
63+
migration_statuses = await self.get_migration_statuses()
64+
return len([i for i in migration_statuses if i.has_ran])
65+
66+
async def havent_ran_count(self) -> int:
67+
"""
68+
:returns:
69+
The number of migrations which haven't been ran.
70+
"""
71+
migration_statuses = await self.get_migration_statuses()
72+
return len([i for i in migration_statuses if not i.has_ran])
73+
74+
async def run(self):
75+
"""
76+
Prints out the migrations which have and haven't ran.
77+
"""
78+
print("Listing migrations ...")
79+
80+
print(
81+
f'{get_fixed_length_string("APP NAME")} | '
82+
f'{get_fixed_length_string("MIGRATION_ID")} | RAN'
83+
)
84+
85+
migration_statuses = await self.get_migration_statuses()
86+
87+
for migration_status in migration_statuses:
88+
fixed_length_app_name = get_fixed_length_string(
89+
migration_status.app_name
90+
)
91+
fixed_length_id = get_fixed_length_string(
92+
migration_status.migration_id
93+
)
94+
has_ran = migration_status.has_ran
95+
print(f"{fixed_length_app_name} | {fixed_length_id} | {has_ran}")
96+
5297

5398
async def check(app_name: str = "all"):
5499
"""

piccolo/apps/user/commands/create.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ def get_is_admin() -> bool:
3333
return admin == "y"
3434

3535

36+
def get_is_superuser() -> bool:
37+
while True:
38+
superuser = input("Superuser? Enter y or n:\n")
39+
if superuser in ("y", "n"):
40+
break
41+
else:
42+
print("Unrecognised option")
43+
44+
return superuser == "y"
45+
46+
3647
def create():
3748
"""
3849
Create a new user.
@@ -49,13 +60,15 @@ def create():
4960
sys.exit("The password is too short")
5061

5162
is_admin = get_is_admin()
63+
is_superuser = get_is_superuser()
5264

5365
user = BaseUser(
5466
username=username,
5567
password=password,
5668
admin=is_admin,
5769
email=email,
5870
active=True,
71+
superuser=is_superuser,
5972
)
6073
user.save().run_sync()
6174

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from piccolo.apps.migrations.auto import MigrationManager
2+
from piccolo.columns.column_types import Boolean
3+
from piccolo.columns.column_types import Timestamp
4+
from piccolo.columns.indexes import IndexMethod
5+
6+
7+
ID = "2021-04-30T16:14:15"
8+
VERSION = "0.18.2"
9+
10+
11+
async def forwards():
12+
manager = MigrationManager(migration_id=ID, app_name="user")
13+
14+
manager.add_column(
15+
table_class_name="BaseUser",
16+
tablename="piccolo_user",
17+
column_name="superuser",
18+
column_class_name="Boolean",
19+
column_class=Boolean,
20+
params={
21+
"default": False,
22+
"null": False,
23+
"primary": False,
24+
"key": False,
25+
"unique": False,
26+
"index": False,
27+
"index_method": IndexMethod.btree,
28+
},
29+
)
30+
31+
manager.add_column(
32+
table_class_name="BaseUser",
33+
tablename="piccolo_user",
34+
column_name="last_login",
35+
column_class_name="Timestamp",
36+
column_class=Timestamp,
37+
params={
38+
"default": None,
39+
"null": True,
40+
"primary": False,
41+
"key": False,
42+
"unique": False,
43+
"index": False,
44+
"index_method": IndexMethod.btree,
45+
},
46+
)
47+
48+
return manager

piccolo/apps/user/tables.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import secrets
77
import typing as t
88

9-
from piccolo.columns import Varchar, Boolean, Secret
9+
from piccolo.columns import Varchar, Boolean, Secret, Timestamp
1010
from piccolo.columns.readable import Readable
1111
from piccolo.table import Table
1212
from piccolo.utils.sync import run_sync
@@ -23,7 +23,19 @@ class BaseUser(Table, tablename="piccolo_user"):
2323
last_name = Varchar(null=True)
2424
email = Varchar(length=255, unique=True)
2525
active = Boolean(default=False)
26-
admin = Boolean(default=False)
26+
admin = Boolean(
27+
default=False, help_text="An admin can log into the Piccolo admin GUI."
28+
)
29+
superuser = Boolean(
30+
default=False,
31+
help_text=(
32+
"If True, this user can manage other users's passwords in the "
33+
"Piccolo admin GUI."
34+
),
35+
)
36+
last_login = Timestamp(
37+
null=True, default=None, help_text="When this user last logged in."
38+
)
2739

2840
def __init__(self, **kwargs):
2941
"""

piccolo/engine/postgres.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from piccolo.query.base import Query
1515
from piccolo.querystring import QueryString
1616
from piccolo.utils.sync import run_sync
17-
from piccolo.utils.warnings import colored_warning
17+
from piccolo.utils.warnings import colored_warning, colored_string, Level
1818

1919

2020
@dataclass
@@ -284,11 +284,14 @@ async def prep_database(self):
284284
)
285285
except InsufficientPrivilegeError:
286286
print(
287-
f"Unable to create {extension} extension - some "
288-
"functionality may not behave as expected. Make sure your "
289-
"database user has permission to create extensions, or "
290-
"add it manually using "
291-
f'`CREATE EXTENSION "{extension}";`'
287+
colored_string(
288+
f"=> Unable to create {extension} extension - some "
289+
"functionality may not behave as expected. Make sure "
290+
"your database user has permission to create "
291+
"extensions, or add it manually using "
292+
f'`CREATE EXTENSION "{extension}";`',
293+
level=Level.medium,
294+
)
292295
)
293296

294297
###########################################################################

piccolo/main.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33

44
from targ import CLI # type: ignore
55

6-
from piccolo.conf.apps import AppRegistry, Finder
76
from piccolo.apps.app.piccolo_app import APP_CONFIG as app_config
87
from piccolo.apps.asgi.piccolo_app import APP_CONFIG as asgi_config
98
from piccolo.apps.meta.piccolo_app import APP_CONFIG as meta_config
9+
from piccolo.apps.migrations.commands.check import CheckMigrationManager
1010
from piccolo.apps.migrations.piccolo_app import APP_CONFIG as migrations_config
1111
from piccolo.apps.playground.piccolo_app import APP_CONFIG as playground_config
1212
from piccolo.apps.project.piccolo_app import APP_CONFIG as project_config
1313
from piccolo.apps.shell.piccolo_app import APP_CONFIG as shell_config
1414
from piccolo.apps.sql_shell.piccolo_app import APP_CONFIG as sql_shell_config
1515
from piccolo.apps.user.piccolo_app import APP_CONFIG as user_config
16+
from piccolo.conf.apps import AppRegistry, Finder
17+
from piccolo.utils.sync import run_sync
18+
from piccolo.utils.warnings import colored_string, Level
1619

1720

1821
DIAGNOSE_FLAG = "--diagnose"
@@ -81,6 +84,37 @@ def main():
8184
continue
8285
cli.register(command, group_name=app_name)
8386

87+
if "migrations" not in sys.argv:
88+
# Show a warning if any migrations haven't been run.
89+
# Don't run it if it looks like the user is running a migration
90+
# command, as this information is redundant.
91+
92+
try:
93+
havent_ran_count = run_sync(
94+
CheckMigrationManager(app_name="all").havent_ran_count()
95+
)
96+
if havent_ran_count:
97+
message = (
98+
f"{havent_ran_count} migration hasn't"
99+
if havent_ran_count == 1
100+
else f"{havent_ran_count} migrations haven't"
101+
)
102+
print(
103+
colored_string(
104+
message=(
105+
"=> {} been run - the app "
106+
"might not behave as expected.\n"
107+
"To check which use:\n"
108+
" piccolo migrations check\n"
109+
"To run all migrations:\n"
110+
" piccolo migrations forwards all\n"
111+
).format(message),
112+
level=Level.high,
113+
)
114+
)
115+
except Exception:
116+
pass
117+
84118
###########################################################################
85119

86120
cli.run()

piccolo/utils/warnings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class Level(Enum):
1515
high = colorama.Fore.RED
1616

1717

18+
def colored_string(message: str, level: Level = Level.medium) -> str:
19+
return level.value + message + colorama.Fore.RESET
20+
21+
1822
def colored_warning(
1923
message: str,
2024
category: t.Type[Warning] = Warning,
@@ -36,5 +40,5 @@ def colored_warning(
3640
:level:
3741
Used to determine the colour of the text displayed to the user.
3842
"""
39-
colored_message = level.value + message + colorama.Fore.RESET
43+
colored_message = colored_string(message=message, level=level)
4044
warnings.warn(colored_message, category=category, stacklevel=stacklevel)

piccolo_conf.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
This piccolo_conf file is just here so migrations can be made for Piccolo's own
3+
internal apps.
4+
5+
For example:
6+
7+
python -m piccolo.main migration new user --auto
8+
9+
"""
10+
11+
from piccolo.conf.apps import AppRegistry
12+
from piccolo.engine.postgres import PostgresEngine
13+
14+
15+
DB = PostgresEngine(config={})
16+
17+
18+
# A list of paths to piccolo apps
19+
# e.g. ['blog.piccolo_app']
20+
APP_REGISTRY = AppRegistry(apps=["piccolo.apps.user.piccolo_app"])

tests/apps/migrations/test_migration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55

66
class TestMigrationTable(TestCase):
77
def test_migration_table(self):
8-
Migration.create_table().run_sync()
8+
Migration.create_table(if_not_exists=True).run_sync()
99
Migration.select().run_sync()
1010
Migration.alter().drop_table().run_sync()

tests/apps/user/commands/test_create.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def tearDown(self):
3131
@patch(
3232
"piccolo.apps.user.commands.create.get_is_admin", return_value=True,
3333
)
34+
@patch(
35+
"piccolo.apps.user.commands.create.get_is_superuser",
36+
return_value=True,
37+
)
3438
def test_create(self, *args, **kwargs):
3539
create()
3640

@@ -40,6 +44,7 @@ def test_create(self, *args, **kwargs):
4044
(BaseUser.admin == True) # noqa: E712
4145
& (BaseUser.username == "bob123")
4246
& (BaseUser.email == "[email protected]")
47+
& (BaseUser.superuser == True)
4348
)
4449
.run_sync()
4550
)

0 commit comments

Comments
 (0)