Skip to content

Commit 488b145

Browse files
didomdfcoding
authored andcommitted
Generic events (pypi#8324)
* Generic events * Update migration to rename table/columns in place * Use AbstractConcreteBase * Address feedback from review * Remove commented out line * Linting
1 parent 8385e2d commit 488b145

File tree

11 files changed

+183
-123
lines changed

11 files changed

+183
-123
lines changed

tests/common/db/accounts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import factory
1616

17-
from warehouse.accounts.models import Email, User, UserEvent
17+
from warehouse.accounts.models import Email, User
1818

1919
from .base import WarehouseFactory
2020

@@ -42,9 +42,9 @@ class Meta:
4242

4343
class UserEventFactory(WarehouseFactory):
4444
class Meta:
45-
model = UserEvent
45+
model = User.Event
4646

47-
user = factory.SubFactory(User)
47+
source = factory.SubFactory(User)
4848

4949

5050
class EmailFactory(WarehouseFactory):

tests/common/db/packaging.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
JournalEntry,
2626
ProhibitedProjectName,
2727
Project,
28-
ProjectEvent,
2928
Release,
3029
Role,
3130
RoleInvitation,
@@ -48,9 +47,9 @@ class Meta:
4847

4948
class ProjectEventFactory(WarehouseFactory):
5049
class Meta:
51-
model = ProjectEvent
50+
model = Project.Event
5251

53-
project = factory.SubFactory(ProjectFactory)
52+
source = factory.SubFactory(ProjectFactory)
5453

5554

5655
class DescriptionFactory(WarehouseFactory):

tests/unit/accounts/test_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ def test_query_by_email_when_not_primary(self, db_session):
102102

103103
def test_recent_events(self, db_session):
104104
user = DBUserFactory.create()
105-
recent_event = DBUserEventFactory(user=user, tag="foo", ip_address="0.0.0.0")
105+
recent_event = DBUserEventFactory(source=user, tag="foo", ip_address="0.0.0.0")
106106
stale_event = DBUserEventFactory(
107-
user=user,
107+
source=user,
108108
tag="bar",
109109
ip_address="0.0.0.0",
110110
time=datetime.datetime.now() - datetime.timedelta(days=91),

tests/unit/manage/test_views.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
File,
4949
JournalEntry,
5050
Project,
51-
ProjectEvent,
5251
Role,
5352
RoleInvitation,
5453
User,
@@ -2556,12 +2555,9 @@ def test_toggle_2fa_requirement_non_critical(
25562555
assert result.status_code == 303
25572556
assert result.headers["Location"] == "/foo/bar/"
25582557

2559-
event = (
2560-
db_request.db.query(ProjectEvent)
2561-
.join(ProjectEvent.project)
2562-
.filter(ProjectEvent.project_id == project.id)
2563-
.one()
2564-
)
2558+
events = project.events
2559+
assert len(events) == 1
2560+
event = events[0]
25652561
assert event.tag == tag
25662562
assert event.additional == {"modified_by": db_request.user.username}
25672563

@@ -4460,13 +4456,13 @@ class TestManageProjectHistory:
44604456
def test_get(self, db_request):
44614457
project = ProjectFactory.create()
44624458
older_event = ProjectEventFactory.create(
4463-
project=project,
4459+
source=project,
44644460
tag="fake:event",
44654461
ip_address="0.0.0.0",
44664462
time=datetime.datetime(2017, 2, 5, 17, 18, 18, 462_634),
44674463
)
44684464
newer_event = ProjectEventFactory.create(
4469-
project=project,
4465+
source=project,
44704466
tag="fake:event",
44714467
ip_address="0.0.0.0",
44724468
time=datetime.datetime(2018, 2, 5, 17, 18, 18, 462_634),
@@ -4510,13 +4506,13 @@ def test_first_page(self, db_request):
45104506
total_items = items_per_page + 2
45114507
for _ in range(total_items):
45124508
ProjectEventFactory.create(
4513-
project=project, tag="fake:event", ip_address="0.0.0.0"
4509+
source=project, tag="fake:event", ip_address="0.0.0.0"
45144510
)
45154511
events_query = (
4516-
db_request.db.query(ProjectEvent)
4517-
.join(ProjectEvent.project)
4518-
.filter(ProjectEvent.project_id == project.id)
4519-
.order_by(ProjectEvent.time.desc())
4512+
db_request.db.query(Project.Event)
4513+
.join(Project.Event.source)
4514+
.filter(Project.Event.source_id == project.id)
4515+
.order_by(Project.Event.time.desc())
45204516
)
45214517

45224518
events_page = SQLAlchemyORMPage(
@@ -4541,13 +4537,13 @@ def test_last_page(self, db_request):
45414537
total_items = items_per_page + 2
45424538
for _ in range(total_items):
45434539
ProjectEventFactory.create(
4544-
project=project, tag="fake:event", ip_address="0.0.0.0"
4540+
source=project, tag="fake:event", ip_address="0.0.0.0"
45454541
)
45464542
events_query = (
4547-
db_request.db.query(ProjectEvent)
4548-
.join(ProjectEvent.project)
4549-
.filter(ProjectEvent.project_id == project.id)
4550-
.order_by(ProjectEvent.time.desc())
4543+
db_request.db.query(Project.Event)
4544+
.join(Project.Event.source)
4545+
.filter(Project.Event.source_id == project.id)
4546+
.order_by(Project.Event.time.desc())
45514547
)
45524548

45534549
events_page = SQLAlchemyORMPage(
@@ -4572,7 +4568,7 @@ def test_raises_404_with_out_of_range_page(self, db_request):
45724568
total_items = items_per_page + 2
45734569
for _ in range(total_items):
45744570
ProjectEventFactory.create(
4575-
project=project, tag="fake:event", ip_address="0.0.0.0"
4571+
source=project, tag="fake:event", ip_address="0.0.0.0"
45764572
)
45774573

45784574
with pytest.raises(HTTPNotFound):

warehouse/accounts/models.py

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@
3030
select,
3131
sql,
3232
)
33-
from sqlalchemy.dialects.postgresql import JSONB, UUID
33+
from sqlalchemy.dialects.postgresql import UUID
3434
from sqlalchemy.ext.hybrid import hybrid_property
3535
from sqlalchemy.orm.exc import NoResultFound
3636

3737
from warehouse import db
38+
from warehouse.events.models import HasEvents
3839
from warehouse.sitemap.models import SitemapMixin
3940
from warehouse.utils.attrs import make_repr
4041
from warehouse.utils.db.types import TZDateTime
@@ -57,7 +58,7 @@ class DisableReason(enum.Enum):
5758
AccountFrozen = "account frozen"
5859

5960

60-
class User(SitemapMixin, db.Model):
61+
class User(SitemapMixin, HasEvents, db.Model):
6162

6263
__tablename__ = "users"
6364
__table_args__ = (
@@ -107,20 +108,6 @@ class User(SitemapMixin, db.Model):
107108
"Macaroon", backref="user", cascade="all, delete-orphan", lazy=True
108109
)
109110

110-
events = orm.relationship(
111-
"UserEvent", backref="user", cascade="all, delete-orphan", lazy=True
112-
)
113-
114-
def record_event(self, *, tag, ip_address, additional):
115-
session = orm.object_session(self)
116-
event = UserEvent(
117-
user=self, tag=tag, ip_address=ip_address, additional=additional
118-
)
119-
session.add(event)
120-
session.flush()
121-
122-
return event
123-
124111
@property
125112
def primary_email(self):
126113
primaries = [x for x in self.emails if x.primary]
@@ -167,9 +154,11 @@ def recent_events(self):
167154
session = orm.object_session(self)
168155
last_ninety = datetime.datetime.now() - datetime.timedelta(days=90)
169156
return (
170-
session.query(UserEvent)
171-
.filter((UserEvent.user_id == self.id) & (UserEvent.time >= last_ninety))
172-
.order_by(UserEvent.time.desc())
157+
session.query(User.Event)
158+
.filter(
159+
(User.Event.source_id == self.id) & (User.Event.time >= last_ninety)
160+
)
161+
.order_by(User.Event.time.desc())
173162
.all()
174163
)
175164

@@ -217,21 +206,6 @@ class RecoveryCode(db.Model):
217206
burned = Column(DateTime, nullable=True)
218207

219208

220-
class UserEvent(db.Model):
221-
__tablename__ = "user_events"
222-
223-
user_id = Column(
224-
UUID(as_uuid=True),
225-
ForeignKey("users.id", deferrable=True, initially="DEFERRED"),
226-
nullable=False,
227-
index=True,
228-
)
229-
tag = Column(String, nullable=False)
230-
time = Column(DateTime, nullable=False, server_default=sql.func.now())
231-
ip_address = Column(String, nullable=False)
232-
additional = Column(JSONB, nullable=True)
233-
234-
235209
class UnverifyReasons(enum.Enum):
236210

237211
SpamComplaint = "spam complaint"

warehouse/events/models.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from sqlalchemy import Column, DateTime, ForeignKey, String, orm, sql
14+
from sqlalchemy.dialects.postgresql import JSONB, UUID
15+
from sqlalchemy.ext.declarative import AbstractConcreteBase, declared_attr
16+
17+
from warehouse import db
18+
19+
20+
class Event(AbstractConcreteBase):
21+
tag = Column(String, nullable=False)
22+
time = Column(DateTime, nullable=False, server_default=sql.func.now())
23+
ip_address = Column(String, nullable=False)
24+
additional = Column(JSONB, nullable=True)
25+
26+
@declared_attr
27+
def __tablename__(cls): # noqa: N805
28+
return "_".join([cls.__name__.removesuffix("Event").lower(), "events"])
29+
30+
@declared_attr
31+
def __mapper_args__(cls): # noqa: N805
32+
return (
33+
{"polymorphic_identity": cls.__name__, "concrete": True}
34+
if cls.__name__ != "Event"
35+
else {}
36+
)
37+
38+
@declared_attr
39+
def source_id(cls): # noqa: N805
40+
return Column(
41+
UUID(as_uuid=True),
42+
ForeignKey(
43+
"%s.id" % cls._parent_class.__tablename__,
44+
deferrable=True,
45+
initially="DEFERRED",
46+
),
47+
nullable=False,
48+
)
49+
50+
@declared_attr
51+
def source(cls): # noqa: N805
52+
return orm.relationship(cls._parent_class)
53+
54+
def __init_subclass__(cls, /, parent_class, **kwargs):
55+
cls._parent_class = parent_class
56+
return cls
57+
58+
59+
class HasEvents:
60+
def __init_subclass__(cls, /, **kwargs):
61+
super().__init_subclass__(**kwargs)
62+
cls.Event = type(
63+
f"{cls.__name__}Event", (Event, db.Model), dict(), parent_class=cls
64+
)
65+
return cls
66+
67+
@declared_attr
68+
def events(cls): # noqa: N805
69+
return orm.relationship(cls.Event, cascade="all, delete-orphan", lazy=True)
70+
71+
def record_event(self, *, tag, ip_address, additional=None):
72+
session = orm.object_session(self)
73+
event = self.Event(
74+
source=self, tag=tag, ip_address=ip_address, additional=additional
75+
)
76+
session.add(event)
77+
session.flush()
78+
79+
return event

warehouse/locale/messages.pot

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ msgstr ""
116116
msgid "Successful WebAuthn assertion"
117117
msgstr ""
118118

119-
#: warehouse/accounts/views.py:441 warehouse/manage/views.py:814
119+
#: warehouse/accounts/views.py:441 warehouse/manage/views.py:813
120120
msgid "Recovery code accepted. The supplied code cannot be used again."
121121
msgstr ""
122122

@@ -225,51 +225,51 @@ msgstr ""
225225
msgid "Banner Preview"
226226
msgstr ""
227227

228-
#: warehouse/manage/views.py:245
228+
#: warehouse/manage/views.py:244
229229
msgid "Email ${email_address} added - check your email for a verification link"
230230
msgstr ""
231231

232-
#: warehouse/manage/views.py:762
232+
#: warehouse/manage/views.py:761
233233
msgid "Recovery codes already generated"
234234
msgstr ""
235235

236-
#: warehouse/manage/views.py:763
236+
#: warehouse/manage/views.py:762
237237
msgid "Generating new recovery codes will invalidate your existing codes."
238238
msgstr ""
239239

240-
#: warehouse/manage/views.py:1191
240+
#: warehouse/manage/views.py:1190
241241
msgid ""
242242
"There have been too many attempted OpenID Connect registrations. Try "
243243
"again later."
244244
msgstr ""
245245

246-
#: warehouse/manage/views.py:1872
246+
#: warehouse/manage/views.py:1871
247247
msgid "User '${username}' already has ${role_name} role for project"
248248
msgstr ""
249249

250-
#: warehouse/manage/views.py:1883
250+
#: warehouse/manage/views.py:1882
251251
msgid ""
252252
"User '${username}' does not have a verified primary email address and "
253253
"cannot be added as a ${role_name} for project"
254254
msgstr ""
255255

256-
#: warehouse/manage/views.py:1896
256+
#: warehouse/manage/views.py:1895
257257
msgid "User '${username}' already has an active invite. Please try again later."
258258
msgstr ""
259259

260-
#: warehouse/manage/views.py:1954
260+
#: warehouse/manage/views.py:1953
261261
msgid "Invitation sent to '${username}'"
262262
msgstr ""
263263

264-
#: warehouse/manage/views.py:2001
264+
#: warehouse/manage/views.py:2000
265265
msgid "Could not find role invitation."
266266
msgstr ""
267267

268-
#: warehouse/manage/views.py:2012
268+
#: warehouse/manage/views.py:2011
269269
msgid "Invitation already expired."
270270
msgstr ""
271271

272-
#: warehouse/manage/views.py:2036
272+
#: warehouse/manage/views.py:2035
273273
msgid "Invitation revoked from '${username}'."
274274
msgstr ""
275275

0 commit comments

Comments
 (0)