-
Notifications
You must be signed in to change notification settings - Fork 38
/
orm.py
181 lines (141 loc) · 7.21 KB
/
orm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
from datetime import datetime
import alembic.command
import alembic.config
from alembic.script import ScriptDirectory
from tornado.log import app_log
from jupyterhub.orm import Base, Column, Integer, ForeignKey, relationship, JSONDict, Unicode, DateTime, Spawner, Group, User, Boolean
from sqlalchemy.orm import backref
from sqlalchemy import inspect
my_table_names = {'dashboards'}
class Dashboard(Base):
""""Database class for a Dashboard"""
__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
user = relationship(User, backref=backref("dashboards_own", uselist=True, cascade='all, delete-orphan'))
# Which spawner/server is being cloned
source_spawner_id = Column(Integer, ForeignKey('spawners.id', ondelete='SET NULL'))
source_spawner = relationship(Spawner, foreign_keys=[source_spawner_id], backref=backref('dashboard_source_for', uselist=True))
name = Column(Unicode(255))
description = Column(Unicode(255), default='')
urlname = Column(Unicode(255), index=True, unique=True, nullable=False)
created = Column(DateTime, default=datetime.utcnow)
started = Column(DateTime)
start_path = Column(Unicode(255), default='') # E.g. which ipynb file should Voila display
presentation_type = Column(Unicode(255), default='') # Code for framework: voila, streamlit, dash etc
allow_all = Column(Boolean, index=True, default=True)
# The resulting spawner displaying the finished dashboard, once ready
final_spawner_id = Column(Integer, ForeignKey('spawners.id', ondelete='SET NULL'))
final_spawner = relationship(Spawner, foreign_keys=[final_spawner_id], backref=backref('dashboard_final_of', uselist=False))
group_id = Column(Integer, ForeignKey('groups.id', ondelete='SET NULL'))
group = relationship(Group, foreign_keys=[group_id], backref=backref('dashboard_visitors_for', uselist=False))
options = Column(JSONDict)
@property
def groupname(self):
if not self.urlname:
raise Exception('Cannot calculate groupname before urlname is set')
return 'dash-{}'.format(self.urlname)
# properties on the dashboard wrapper
# some APIs get these low-level objects
# when the dashboard isn't running,
# for which these should all be False
active = running = ready = False
pending = None
@property
def orm_dashboard(self):
return self
@classmethod
def find(cls, db, urlname, user=None):
"""Find a Dashboard by urlname.
Returns None if not found.
"""
if user is None:
return db.query(cls).filter(cls.urlname == urlname).first()
return db.query(cls).filter(cls.urlname == urlname, cls.user_id == user.id).first()
def is_orm_user_allowed(self, user):
if user == self.user:
return True
if not self.allow_all:
if not user in self.group.users:
return False
return True
def __iter__(self):
"""
Fix for bug in JH 1.1, which was fixed in JH 1.2 by:
https://github.com/jupyterhub/jupyterhub/commit/84acdd5a7ffb3d79ca04aa86ffcea7e067cce6ee#diff-ea603eff790f5d20b768e093b5340908
(Actually, the real fix was a little bit earlier, but this is the right place).
Without this, delete dashboard causes an error when corresponding relationships were expired.
"""
return iter([self])
class DatabaseSchemaMismatch(Exception):
"""Exception raised when the database schema version does not match
the current version of JupyterHub.
"""
def check_db_revision(engine):
"""Check the JupyterHub database revision
After calling this function, an alembic tag is guaranteed to be stored in the db.
- Checks the alembic tag and raises a ValueError if it's not the current revision
- If no tag is stored (Bug in Hub prior to 0.8),
guess revision based on db contents and tag the revision.
- Empty databases are tagged with the current revision
"""
# Check database schema version
current_table_names = set(engine.table_names())
from .dbutil import _temp_alembic_ini
with _temp_alembic_ini(engine.url) as ini:
cfg = alembic.config.Config(ini)
scripts = ScriptDirectory.from_config(cfg)
head = scripts.get_heads()[0]
base = scripts.get_base()
if not my_table_names.intersection(current_table_names):
# no tables have been created, stamp with current revision
app_log.debug("Stamping empty dashboards database with alembic revision %s", head)
alembic.command.stamp(cfg, head)
return
if 'cds_alembic_version' not in current_table_names:
# Has not been tagged or upgraded before.
# This should only occur for databases created on cdsdashboards 0.0.11 or earlier
# Need to identify if this is really an old version or not
rev = head
if 'dashboards' in current_table_names:
inspector = inspect(engine)
cols = inspector.get_columns('dashboards')
colnames = [c.get('name','') for c in cols]
if not 'presentation_type' in colnames:
rev = base
# presentation_type was added in v0.0.13, so the reason we don't have cds_alembic_version
# is because the db was created before we had db versioning
# If we DO have dashboards.presentation_type but no cds_alembic_version then this is just because
# it's a new installation, Dashboards has been created before this first check.
app_log.debug("Stamping dashboards database schema version %s", rev)
alembic.command.stamp(cfg, rev)
else:
alembic_revision = engine.execute(
'SELECT version_num FROM cds_alembic_version'
).first()[0]
if alembic_revision == base:
if 'dashboards' in current_table_names:
inspector = inspect(engine)
cols = inspector.get_columns('dashboards')
colnames = [c.get('name','') for c in cols]
if 'presentation_type' in colnames:
# For people who got stuck in the broken upgrade before - actually they are NOT on base...
rev = '260ac5c1a9e0'
app_log.debug("Stamping dashboards database schema version %s", rev)
alembic.command.stamp(cfg, rev)
# check database schema version
# it should always be defined at this point
alembic_revision = engine.execute(
'SELECT version_num FROM cds_alembic_version'
).first()[0]
if alembic_revision == head:
app_log.debug("database dashboards schema version found: %s", alembic_revision)
pass
else:
raise DatabaseSchemaMismatch(
"Found database schema version {found} != {head}. "
"Backup your database and run `jupyterhub upgrade-db`"
" to upgrade to the latest schema.".format(
found=alembic_revision, head=head
)
)