Skip to content

Commit e6fcdb1

Browse files
committed
DEV ONLY Issue to join data from other table in hybrid expression in CourseEvent
1 parent eae5dcd commit e6fcdb1

File tree

14 files changed

+301
-40
lines changed

14 files changed

+301
-40
lines changed

src/onegov/ballot/models/election/election.py

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def counted(self) -> bool:
137137

138138
return (sum(1 for r in self.results if r.counted) == count)
139139

140+
# tschupre example for expression
140141
@counted.expression # type:ignore[no-redef]
141142
def counted(cls) -> 'ColumnElement[bool]':
142143
expr = select([

src/onegov/core/orm/session_manager.py

+3
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,9 @@ def ensure_schema_exists(self, schema: str) -> None:
675675
base.metadata.create_all(conn)
676676

677677
declared_classes.update(base._decl_class_registry.values())
678+
# declared_classes.update(
679+
# base.registry._class_registry.values()) # upgrading
680+
# to sqlalchemy 1.4.x
678681

679682
conn.execute('COMMIT')
680683
finally:

src/onegov/directory/models/directory_entry.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ class DirectoryEntry(Base, ContentMixin, CoordinatesMixin, TimestampMixin,
2323
__tablename__ = 'directory_entries'
2424

2525
es_properties = {
26-
'keywords': {'type': 'keyword'},
26+
# 'keywords': {'type': 'keyword'}, # produce a literal issue when
27+
# using sqlalchemy 1.4
2728
'title': {'type': 'localized'},
2829
'lead': {'type': 'localized'},
2930
'_directory_id': {'type': 'keyword'},
3031

3132
# since the searchable text might include html, we remove it
3233
# even if there's no html -> possibly decreasing the search
3334
# quality a bit
34-
'text': {'type': 'localized_html'}
35+
# 'text': {'type': 'localized_html'}
3536
}
3637

3738
@property
@@ -103,14 +104,25 @@ def keywords(self):
103104
def keywords(self, value):
104105
self._keywords = {k: '' for k in value} if value else None
105106

107+
# @property
106108
@hybrid_property
107109
def text(self):
108110
return self.directory.configuration.extract_searchable(self.values)
109111

112+
# @text.expression
113+
# def text(cls):
114+
# expr = ''
115+
# print(f'*** hybrid prop text: {expr}')
116+
# return expr
117+
110118
@property
111119
def values(self):
112120
return self.content and self.content.get('values', {})
113121

122+
@hybrid_property
123+
def values(cls):
124+
return cls.content['values']
125+
114126
@values.setter
115127
def values(self, values):
116128
self.content = self.content or {}

src/onegov/feriennet/models/activity.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from functools import cached_property
22

3+
from sqlalchemy import func, text, select, alias, Column, and_
34
from sqlalchemy.ext.hybrid import hybrid_property
5+
from sqlalchemy.orm import Query, column_property
46

57
from onegov.activity import Activity, ActivityCollection, Occasion
68
from onegov.activity import PublicationRequestCollection
@@ -12,6 +14,7 @@
1214
from onegov.org.models.ticket import OrgTicketMixin
1315
from onegov.search import SearchableContent
1416
from onegov.ticket import handlers, Handler, Ticket
17+
from onegov.user import User
1518

1619

1720
class VacationActivity(Activity, CoordinatesExtension, SearchableContent):
@@ -24,7 +27,7 @@ class VacationActivity(Activity, CoordinatesExtension, SearchableContent):
2427
'title': {'type': 'localized'},
2528
'lead': {'type': 'localized'},
2629
'text': {'type': 'localized_html'},
27-
'organiser': {'type': 'text'}
30+
# 'organiser': {'type': 'text'}
2831
}
2932

3033
@property
@@ -39,8 +42,18 @@ def es_public(self):
3942
def es_skip(self):
4043
return self.state == 'preview'
4144

45+
@hybrid_property
46+
def lead(self):
47+
return self.meta['lead'].astext
48+
49+
@hybrid_property
50+
def text(self):
51+
return self.content['text'].astext
52+
53+
# @property
4254
@hybrid_property
4355
def organiser(self):
56+
print(' *** tschupre property organizer()')
4457
organiser = [
4558
self.user.username,
4659
self.user.realname
@@ -67,6 +80,18 @@ def organiser(self):
6780

6881
return organiser
6982

83+
@organiser.expression
84+
def organiser(cls):
85+
expr = (
86+
Query(User.username, User.realname).
87+
select_from(cls).join(User).
88+
filter(cls.username == User.username).
89+
filter(func.concat_ws(' ', User.username, User.realname)).
90+
as_scalar()
91+
)
92+
print(expr)
93+
return expr.label('organizer')
94+
7095
def ordered_tags(self, request, durations=None):
7196
tags = [request.translate(_(tag)) for tag in self.tags]
7297

src/onegov/fsi/models/course_attendee.py

+37-6
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22

33
from onegov.core.orm import Base
44
from onegov.core.orm.types import UUID, JSON
5-
from sqlalchemy import Boolean
5+
from onegov.user import User
6+
from sqlalchemy import Boolean, case, select, and_, func
67
from onegov.search import ORMSearchable
78
from sedate import utcnow
89
from sqlalchemy import Column, Text, ForeignKey, ARRAY, desc
910
from sqlalchemy.orm import relationship, object_session, backref
1011
from uuid import uuid4
1112

12-
1313
from typing import TYPE_CHECKING
14+
1415
if TYPE_CHECKING:
1516
from onegov.user import User
1617
from .course_subscription import CourseSubscription
1718

18-
1919
external_attendee_org = "Externe Kursteilnehmer"
2020

2121

@@ -43,8 +43,8 @@ class CourseAttendee(Base, ORMSearchable):
4343
'first_name': {'type': 'text'},
4444
'last_name': {'type': 'text'},
4545
'organisation': {'type': 'text'},
46-
'email': {'type': 'text'},
47-
'title': {'type': 'text'},
46+
# 'email': {'type': 'text'},
47+
# 'title': {'type': 'text'},
4848
}
4949

5050
es_public = False
@@ -118,6 +118,21 @@ def title(self):
118118
) if p
119119
)) or self.email
120120

121+
@title.expression
122+
def title(cls):
123+
return case((
124+
(
125+
and_(
126+
cls.first_name != '',
127+
cls.last_name != ''
128+
),
129+
# cls.first_name + ' ' + cls.last_name
130+
func.concat(cls.first_name + ' ' + cls.last_name)
131+
)
132+
),
133+
else_=cls.email,
134+
)
135+
121136
@property
122137
def lead(self):
123138
return self.organisation
@@ -140,6 +155,22 @@ def email(self):
140155
return self._email
141156
return self.user.username
142157

158+
@email.expression
159+
def email(cls):
160+
from onegov.user import User
161+
162+
return case((
163+
(
164+
# cls.user_id.isnot(None),
165+
cls.user_id != '',
166+
cls._email
167+
),
168+
), else_=(
169+
select([User.username]).
170+
where(cls.user_id == User.id).
171+
label('email'))
172+
)
173+
143174
@email.setter
144175
def email(self, value):
145176
self._email = value
@@ -194,7 +225,7 @@ def repeating_courses(self):
194225

195226
session = object_session(self)
196227

197-
result = session.query(CourseEvent).join(Course)\
228+
result = session.query(CourseEvent).join(Course) \
198229
.filter(Course.mandatory_refresh == True)
199230

200231
result = result.join(CourseSubscription)

src/onegov/fsi/models/course_event.py

+49-5
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@
77
from icalendar import Event as vEvent
88
from sedate import utcnow, to_timezone
99
from sqlalchemy import (
10-
Column, Boolean, SmallInteger, Enum, Text, Interval, ForeignKey, or_, and_)
10+
Column, Boolean, SmallInteger, Enum, Text, Interval, ForeignKey, or_, and_,
11+
select)
1112
from sqlalchemy.ext.hybrid import hybrid_property
12-
from sqlalchemy.orm import relationship, backref, object_session
13+
from sqlalchemy.orm import relationship, backref, object_session, join
1314
from uuid import uuid4
1415

1516
from onegov.core.mail import Attachment
1617
from onegov.core.orm import Base
1718
from onegov.core.orm.mixins import TimestampMixin
1819
from onegov.core.orm.types import UUID, UTCDateTime
1920
from onegov.fsi import _
21+
from onegov.fsi.models.course import Course
2022
from onegov.fsi.models.course_attendee import CourseAttendee
2123
from onegov.fsi.models.course_subscription import CourseSubscription
2224
from onegov.fsi.models.course_subscription import subscription_table
2325
from onegov.search import ORMSearchable
2426

2527
from typing import TYPE_CHECKING
28+
2629
if TYPE_CHECKING:
2730
from .course import Course
2831
from .course_notification_template import (
@@ -40,7 +43,6 @@
4043

4144
# for forms...
4245
def course_status_choices(request=None, as_dict=False):
43-
4446
if request:
4547
translations = (
4648
request.translate(v) for v in COURSE_EVENT_STATUSES_TRANSLATIONS)
@@ -54,14 +56,14 @@ def course_status_choices(request=None, as_dict=False):
5456

5557

5658
class CourseEvent(Base, TimestampMixin, ORMSearchable):
57-
5859
default_reminder_before = datetime.timedelta(days=14)
5960

6061
__tablename__ = 'fsi_course_events'
6162

6263
es_properties = {
64+
# expression for name and desc implementiert aber subquery issue
6365
'name': {'type': 'localized'},
64-
'description': {'type': 'localized'},
66+
# 'description': {'type': 'localized'},
6567
'location': {'type': 'localized'},
6668
'presenter_name': {'type': 'text'},
6769
'presenter_company': {'type': 'text'},
@@ -89,6 +91,41 @@ def title(self):
8991
def name(self):
9092
return self.course.name
9193

94+
@name.expression
95+
def name(cls):
96+
# both variants lead to the same error:
97+
# sqlalchemy.exc.NotSupportedError: (psycopg2.errors.FeatureNotSupported) cannot use subquery in column generation expression
98+
# subquery issue probably produced due where clause
99+
100+
# expr = select([Course.name])
101+
# expr = expr.where(Course.id == cls.course_id)
102+
# expr = expr.label('name')
103+
# return expr
104+
105+
# expr = (select([Course.name]).where(Course.id == cls.course_id).
106+
# select_from(join(cls, Course)).label('name'))
107+
108+
# the variant below are closer to the final version (explicit
109+
# join) but throw: 'Can't find any foreign key relationships between
110+
# 'Select object' and 'fsi_courses'
111+
expr = select([Course.name]).select_from(cls).join(
112+
Course).where(Course.id == cls.course_id).label('name')
113+
114+
# throws: subquery in FROM must-have an alias
115+
# j = select([cls.course_id]).join(Course, Course.id ==
116+
# cls.course_id).alias('j')
117+
# expr = select([Course.name]).select_from(j).label('name')
118+
119+
# example https://docs.sqlalchemy.org/en/20/orm/queryguide/select.html#joining-to-subqueries
120+
# throwing: TypeError: 'DeclarativeMeta' object is not iterable
121+
# subq = select(Address).where(Address.email_address == "[email protected]").subquery()
122+
# stmt = select(User).join(subq, User.id == subq.c.user_id)
123+
# subquery = select(cls).where(cls.course_id == Course.id).subquery()
124+
# expr = select(Course).join(subquery, Course.id == subquery.course_id)
125+
126+
print(expr)
127+
return expr
128+
92129
@property
93130
def lead(self):
94131
return (
@@ -101,6 +138,13 @@ def lead(self):
101138
def description(self):
102139
return self.course.description
103140

141+
@description.expression
142+
def description(cls):
143+
expr = select([Course.description])
144+
expr = expr.where(Course.id == cls.course_id)
145+
expr = expr.label('description')
146+
return expr
147+
104148
def __str__(self):
105149
start = to_timezone(
106150
self.start, 'Europe/Zurich').strftime('%d.%m.%Y %H:%M')

src/onegov/org/models/page.py

+4
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class Topic(Page, TraitInfo, SearchableContent, AccessExtension,
4545
lead_when_child = content_property(default=True)
4646

4747
@hybrid_property
48+
def lead(self): # noqa: F811
49+
return self.content['lead']
50+
51+
@lead.expression
4852
def lead(self): # noqa: F811
4953
return self.content['lead'].astext
5054

0 commit comments

Comments
 (0)