Ticket #1028: new_models_r1.org

File new_models_r1.org, 11.5 KB (added by Jessica Tallon, 9 years ago)
Line 
1#+TITLE: New Models (GNU MediaGoblin)
2#+AUTHOR: Jessica Tallon <jessica@megworld.co.uk>
3* Generic Foreign Key
4 There is a few solutions to this including one example from SQLAlchemy
5 themselves. I personlly think that the SA's [[http://docs.sqlalchemy.org/en/latest/_modules/examples/generic_associations/generic_fk.html][example]] isn't flexible enough
6 to work as a truely generic way of easily having 1 or more foreign keys on
7 a model.
8
9 We don't need referential integrity, I'm thinking something like:
10 #+BEGIN_SRC python
11 from sqlalchemy.orm import class_mapper
12
13 class GenericModelReference(Base):
14 """
15 This represents a generic relationship to any model
16 that is defined with a integer primry key.
17 """
18 __tablename__ = "core__generic_model_reference"
19
20 id = Column(Integer, primary_key=True)
21
22 obj_pk = Column(Integer, nullable=False)
23
24 # This will be the tablename of the model
25 model_type = Column(Unicode, nullable=False)
26
27 @property
28 def get(self):
29 # This can happen if it's yet to be saved
30 if self.model_type is None or self.obj_pk is None:
31 return None
32
33 model = self._get_model_from_type(self.model_type)
34 return model.query.filter_by(id=self.obj_pk)
35
36 @property
37 def set(self, obj):
38 model = obj.__class__
39
40 # Check we've been given a object
41 if not issubclass(model, Base):
42 raise ValueError("Only models can be set as GenericForeignKeys")
43
44 # Check that the model has an explicit __tablename__ declaration
45 if getattr(model, "__tablename__", None) is None:
46 raise ValueError("Models must have __tablename__ attribute")
47
48 # Check that it's not a composite primary key
49 primary_keys = [key.name for key in class_mapper(model).primary_key]
50 if len(primary_keys) > 1:
51 raise ValueError("Models can't have composite primary keys")
52
53 # Check that the field on the model is a an integer field
54 pk_column = getattr(model, primary_keys[0])
55 if issubclass(Integer, pk_column):
56 raise ValueError("Only models with integer pks can be set")
57
58 # Ensure that everything has it's ID set
59 obj.save(commit=False)
60
61 self.obj_pk = obj.id
62 self.model_type = obj.__tablename__
63
64
65 def _get_model_from_type(self, model_type):
66 """ Gets a model from a tablename (model type) """
67 if getattr(self, "_TYPE_MAP", None) is None:
68 # We want to build on the class (not the instance) a map of all the
69 # models by the table name (type) for easy lookup, this is done on
70 # the class so it can be shared between all instances
71
72 # to prevent circular imports do import here
73 from mediagoblin.db.models import MODELS
74 self._TYPE_MAP = dict(((m.__tablename__, m) for m in MODELS))
75 setattr(self.__class__._TYPE_MAP, self._TYPE_MAP)
76
77 return self._TYPE_MAP[model_type]
78
79
80 class GenericForeignKey(ForeignKey):
81
82 def __init__(self, *args, **kwargs):
83 super(GenericForeignKey, self).__init__(
84 "core__generic_model_reference.id",
85 ,*args,
86 ,**kwargs
87 )
88
89 #+END_SRC
90* User model
91 Use the [[http://docs.sqlalchemy.org/en/rel_0_8/orm/extensions/declarative.html#inheritance-configuration][inheritance configuration]] that will allow us to have a base User model
92 which a Local and Remote user can extend from. This allows us to have the local
93 specific attributes on the LocalUser and the remote specific on the Remote user
94 but retain a single User model to point to and retain referential integrity.
95
96 #+BEGIN_SRC python
97 class User(Base):
98 """
99 The base User class that LocalUser and RemoteUser
100 can inherit from. This will hold all the fields which
101 are shared between all User models.
102
103 NB: In ForeignKey fields to User you should point to this
104 User model not the LocalUser or RemoteUser models.
105 """
106 __tablename__ = "core__users"
107
108 id = Column(Integer, primary_key=True)
109 public_id = Column(Unicode, unique=True)
110
111 name = Column(Unicode)
112 bio = Column(UnicodeText)
113 url = Column(Unicode)
114 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
115 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
116 location = Column(Integer, ForeignKey("core__locations.id"))
117
118 # Lazy getters
119 get_location = relationship("Location", lazy="joined")
120
121 class LocalUser(User):
122 """
123 These are Users which exist on this instance.
124 """
125 __tablename__ = "core__local_users"
126
127 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
128
129 username = Column(Unicode, nullable=False, unique=True)
130 email = Column(Unicode, nullable=False)
131 pw_hash = Column(Unicode)
132
133 wants_comment_notification = Column(Boolean, default=True)
134 wants_notifications = Column(Boolean, default=True)
135 license_preference = Column(Unicode)
136 uploaded = Column(Integer, default=0)
137 upload_limit = Column(Integer)
138
139 def __repr__(self):
140 return "<{0} #{1} {2} {3} {4}>".format(
141 self.__class__.__name__,
142 self.id,
143 "verified" if self.has_privilege(u"active") else "non-verified",
144 "admin" if self.has_privilege(u"admin") else "user",
145 self.username
146 )
147
148 class RemoteUser(User):
149 """
150 These are Users which are registered on a remote site
151 """
152 __tablename__ = "core__remote_users"
153
154 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
155
156 webfinger = Column(Unicode, nullable=False)
157
158 def __repr__(self):
159 return "<{0} #{1} {2}>".format(
160 self.__class__.__name__,
161 self.id,
162 self.webfinger
163 )
164 #+END_SRC
165
166* MediaEntry
167 #+BEGIN_SRC python
168 class MediaEntry(Base, MediaEntryMixin):
169 __tablename__ = "core__media_entries"
170
171 id = Column(Integer, primary_key=True)
172 public_id = Column(Unicode, unique=True)
173 deleted = Column(Boolean, default=False)
174 remote = Column(Boolean, default=False)
175
176 title = Column(Unicode)
177 slug = Column(Unicode)
178 description = Column(UnicodeText)
179 media_type = Column(Unicode, nullable=False)
180 state = Column(Unicode, default=u"unprocessed", nullable=False)
181 license = Column(Unicode)
182 file_size = Column(Integer, default=0)
183 created = Column(DateTime, default=datetime.datetime.now, nullable=False)
184 updated = Column(DateTime, default=datetime.datetime.now, nullable=False)
185
186 uploader = Column(Integer, ForeignKey("core__users.id"), nullable=False, index=True)
187 #+END_SRC
188* Activity
189 The activity needs to/cc/bto/bcc needs to be able to reference
190 Users, Lists of users and a special list users such as:
191
192 - Public
193 - Following
194 - Followers
195
196 This should be a Generic ManyToMany field. The activity should be:
197 #+BEGIN_SRC python
198 # Create the link tables for the ManyToMany fields
199 class ToLinkTable(Base):
200 """ Link table for Activity.to """
201 __tablename__ = "link__activity_to"
202
203 activity = Column(Integer, ForeignKey("core__activities.id"))
204 obj = Column(Integer, GenericForeignKey())
205
206 class CCLinkTable(Base):
207 """ Link table for Activity.cc """
208 __tablename__ = "link__activity_cc"
209
210 activity = Column(Integer, ForeignKey("core__activities.id"))
211 obj = Column(Integer, GenericForeignKey())
212
213 class BToLinkTable(Base):
214 """ Link table for Activity.bto """
215 __tablename__ = "link__activity_bto"
216
217 activity = Column(Integer, ForeignKey("core__activities.id"))
218 obj = Column(Integer, GenericForeignKey())
219
220 class BCCLinkTable(Base):
221 """ Link table for Activity.bcc """
222 __tablename__ = "link__activity_bcc"
223
224 activity = Column(Integer, ForeignKey("core__activities.id"))
225 obj = Column(Integer, GenericForeignKey())
226
227 class Activity(Base):
228 __tablename__ = "core__activities"
229
230 id = Column(Integer, primary_key=True)
231 public_id = Column(Integer, unique=True)
232 remote = Column(Boolean, default=False)
233
234 to = relationship("GenericModelReference",
235 secondary=to_link_table)
236 cc = relationship("GenericModelReference",
237 secondary=cc_link_table)
238 bto = relationship("GenericModelReference",
239 secondary=bto_link_table)
240 bcc = relationship("GenericModelReference",
241 secondary=bcc_link_table)
242
243 actor = Column(Integer, ForeignKey("core__users.id"), nullable=False)
244 generator = Column(Integer,
245 ForeignKey("core__generators.id"),
246 nullable=False)
247 published = Column(DateTimeField,
248 nullable=False,
249 default=datetime.datetime.now)
250 updated = Column(DateTimeField,
251 nullable=False,
252 default=datetime.datetime.now)
253
254 title = Column(Unicode)
255 content = Column(UnicodeText)
256 verb = Column(Unicode)
257
258 object = Column(Integer, GenericForeignKey(), nullable=False)
259 target = Column(Integer, GenericForeignKey(), nullable=True)
260
261 #+END_SRC python
262
263* Collections
264 Collections in pump.io can contain any kind of data including different
265 kinds of data ([[https://github.com/activitystreams/activity-schema/blob/master/activity-schema.md#object-types][see spec]]). This will require a migration away from the
266 current kind of Collections which can currently can only be MediaEntries.
267
268 #+BEGIN_SRC python
269 class CollectionLink(Base):
270 """ Link table for contents of collection """
271 __tablename__ = "link__collection_objects"
272
273 collection = Column(Integer, ForeignKey("core__collections"))
274 obj = Column(Integer, GenericForeignKey())
275
276 class Collection(Base):
277 """ This represents a Collection of objects """
278 __tablename__ = "core__collections"
279
280 id = Column(Integer, primary_key=True)
281 public_id = Column(Unicode, unique=True)
282 remote = Column(Boolean, default=False)
283 deleted = Column(Boolean, default=False)
284
285 name = Column(Unicode)
286 description = Column(UnicodeText)
287 objects = relationship("GenericModelReference",
288 secondary=collection_link_table)
289 #+END_SRC
290
291* Likes
292 This is to record who likes a comment or a piece of media.
293
294 #+BEGIN_SRC python
295 class Like(Base):
296 """ This represents a like/favorite on an object """
297 __tablename__ = "core__likes"
298
299 id = Column(Integer, primary_key=True)
300
301 object = Column(Integer, GenericForeignKey(), nullable=False)
302 author = Column(Integer, FoeignKey("core__users.id"), nullable=False)
303 created = Column(DateTimeField, default=datetime.datetime.now, nullable=False)
304 #+END_SRC