Commit | Line | Data |
---|---|---|
8e1e744d | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
cf29e8a8 | 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
e5572c60 ML |
3 | # |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU Affero General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU Affero General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU Affero General Public License | |
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
25bdf9b6 | 17 | import datetime |
4ad5af85 | 18 | |
c2ddd85e | 19 | from mongokit import Document |
4329be14 | 20 | |
25bdf9b6 AW |
21 | from mediagoblin.db.mongo import migrations |
22 | from mediagoblin.db.mongo.util import ASCENDING, DESCENDING, ObjectId | |
152a3bfa | 23 | from mediagoblin.tools.pagination import Pagination |
feba5c52 | 24 | from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin |
d232e0f6 | 25 | |
58f96a13 E |
26 | |
27 | class MongoPK(object): | |
28 | """An alias for the _id primary key""" | |
29 | def __get__(self, instance, cls): | |
30 | return instance['_id'] | |
31 | def __set__(self, instance, val): | |
32 | instance['_id'] = val | |
33 | def __delete__(self, instance): | |
34 | del instance['_id'] | |
35 | ||
36 | ||
7bf3f5db CAW |
37 | ################### |
38 | # Custom validators | |
39 | ################### | |
40 | ||
41 | ######## | |
42 | # Models | |
43 | ######## | |
44 | ||
45 | ||
25bdf9b6 | 46 | class User(Document, UserMixin): |
16bcd1e7 CAW |
47 | """ |
48 | A user of MediaGoblin. | |
49 | ||
50 | Structure: | |
51 | - username: The username of this user, should be unique to this instance. | |
52 | - email: Email address of this user | |
53 | - created: When the user was created | |
54 | - plugin_data: a mapping of extra plugin information for this User. | |
55 | Nothing uses this yet as we don't have plugins, but someday we | |
56 | might... :) | |
57 | - pw_hash: Hashed version of user's password. | |
58 | - email_verified: Whether or not the user has verified their email or not. | |
59 | Most parts of the site are disabled for users who haven't yet. | |
60 | - status: whether or not the user is active, etc. Currently only has two | |
61 | values, 'needs_email_verification' or 'active'. (In the future, maybe | |
62 | we'll change this to a boolean with a key of 'active' and have a | |
63 | separate field for a reason the user's been disabled if that's | |
64 | appropriate... email_verified is already separate, after all.) | |
65 | - verification_key: If the user is awaiting email verification, the user | |
66 | will have to provide this key (which will be encoded in the presented | |
67 | URL) in order to confirm their email as active. | |
68 | - is_admin: Whether or not this user is an administrator or not. | |
69 | - url: this user's personal webpage/website, if appropriate. | |
70 | - bio: biography of this user (plaintext, in markdown) | |
16bcd1e7 | 71 | """ |
73a6e206 | 72 | __collection__ = 'users' |
25bdf9b6 | 73 | use_dot_notation = True |
73a6e206 | 74 | |
d232e0f6 CAW |
75 | structure = { |
76 | 'username': unicode, | |
24181820 | 77 | 'email': unicode, |
d232e0f6 | 78 | 'created': datetime.datetime, |
25bdf9b6 | 79 | 'plugin_data': dict, # plugins can dump stuff here. |
d232e0f6 | 80 | 'pw_hash': unicode, |
24181820 | 81 | 'email_verified': bool, |
4d75522b | 82 | 'status': unicode, |
18cf34d4 CAW |
83 | 'verification_key': unicode, |
84 | 'is_admin': bool, | |
25bdf9b6 AW |
85 | 'url': unicode, |
86 | 'bio': unicode, # May contain markdown | |
25bdf9b6 AW |
87 | 'fp_verification_key': unicode, # forgotten password verification key |
88 | 'fp_token_expire': datetime.datetime, | |
d232e0f6 CAW |
89 | } |
90 | ||
db5912e3 | 91 | required_fields = ['username', 'created', 'pw_hash', 'email'] |
fc9bb821 CAW |
92 | |
93 | default_values = { | |
24181820 | 94 | 'created': datetime.datetime.utcnow, |
4d75522b | 95 | 'email_verified': False, |
db1a438f | 96 | 'status': u'needs_email_verification', |
18cf34d4 | 97 | 'is_admin': False} |
080a81ec | 98 | |
58f96a13 E |
99 | id = MongoPK() |
100 | ||
d232e0f6 | 101 | |
25bdf9b6 | 102 | class MediaEntry(Document, MediaEntryMixin): |
080a81ec CAW |
103 | """ |
104 | Record of a piece of media. | |
105 | ||
106 | Structure: | |
107 | - uploader: A reference to a User who uploaded this. | |
108 | ||
109 | - title: Title of this work | |
110 | ||
111 | - slug: A normalized "slug" which can be used as part of a URL to retrieve | |
112 | this work, such as 'my-works-name-in-slug-form' may be viewable by | |
113 | 'http://mg.example.org/u/username/m/my-works-name-in-slug-form/' | |
114 | Note that since URLs are constructed this way, slugs must be unique | |
115 | per-uploader. (An index is provided to enforce that but code should be | |
116 | written on the python side to ensure this as well.) | |
117 | ||
118 | - created: Date and time of when this piece of work was uploaded. | |
119 | ||
120 | - description: Uploader-set description of this work. This can be marked | |
121 | up with MarkDown for slight fanciness (links, boldness, italics, | |
122 | paragraphs...) | |
123 | ||
080a81ec CAW |
124 | - media_type: What type of media is this? Currently we only support |
125 | 'image' ;) | |
126 | ||
127 | - media_data: Extra information that's media-format-dependent. | |
128 | For example, images might contain some EXIF data that's not appropriate | |
129 | to other formats. You might store it like: | |
130 | ||
25bdf9b6 | 131 | mediaentry.media_data['exif'] = { |
080a81ec CAW |
132 | 'manufacturer': 'CASIO', |
133 | 'model': 'QV-4000', | |
134 | 'exposure_time': .659} | |
135 | ||
136 | Alternately for video you might store: | |
137 | ||
138 | # play length in seconds | |
25bdf9b6 | 139 | mediaentry.media_data['play_length'] = 340 |
080a81ec CAW |
140 | |
141 | ... so what's appropriate here really depends on the media type. | |
142 | ||
143 | - plugin_data: a mapping of extra plugin information for this User. | |
144 | Nothing uses this yet as we don't have plugins, but someday we | |
145 | might... :) | |
146 | ||
147 | - tags: A list of tags. Each tag is stored as a dictionary that has a key | |
148 | for the actual name and the normalized name-as-slug, so ultimately this | |
149 | looks like: | |
150 | [{'name': 'Gully Gardens', | |
151 | 'slug': 'gully-gardens'}, | |
152 | {'name': 'Castle Adventure Time?!", | |
153 | 'slug': 'castle-adventure-time'}] | |
154 | ||
155 | - state: What's the state of this file? Active, inactive, disabled, etc... | |
156 | But really for now there are only two states: | |
157 | "unprocessed": uploaded but needs to go through processing for display | |
158 | "processed": processed and able to be displayed | |
159 | ||
a6c49d49 AW |
160 | - license: URI for media's license. |
161 | ||
080a81ec CAW |
162 | - queued_media_file: storage interface style filepath describing a file |
163 | queued for processing. This is stored in the mg_globals.queue_store | |
164 | storage system. | |
165 | ||
6b9ee0ca CAW |
166 | - queued_task_id: celery task id. Use this to fetch the task state. |
167 | ||
080a81ec CAW |
168 | - media_files: Files relevant to this that have actually been processed |
169 | and are available for various types of display. Stored like: | |
170 | {'thumb': ['dir1', 'dir2', 'pic.png'} | |
171 | ||
172 | - attachment_files: A list of "attachment" files, ones that aren't | |
173 | critical to this piece of media but may be usefully relevant to people | |
174 | viewing the work. (currently unused.) | |
6c50c210 | 175 | |
25bdf9b6 AW |
176 | - fail_error: path to the exception raised |
177 | - fail_metadata: | |
080a81ec | 178 | """ |
4d75522b | 179 | __collection__ = 'media_entries' |
25bdf9b6 | 180 | use_dot_notation = True |
4d75522b CAW |
181 | |
182 | structure = { | |
757f37a5 | 183 | 'uploader': ObjectId, |
4d75522b | 184 | 'title': unicode, |
1013bdaf | 185 | 'slug': unicode, |
4d75522b | 186 | 'created': datetime.datetime, |
25bdf9b6 | 187 | 'description': unicode, # May contain markdown/up |
4d75522b | 188 | 'media_type': unicode, |
25bdf9b6 AW |
189 | 'media_data': dict, # extra data relevant to this media_type |
190 | 'plugin_data': dict, # plugins can dump stuff here. | |
0712a06d | 191 | 'tags': [dict], |
74ae6b11 | 192 | 'state': unicode, |
a6c49d49 | 193 | 'license': unicode, |
74ae6b11 | 194 | |
fa7f9c61 CAW |
195 | # For now let's assume there can only be one main file queued |
196 | # at a time | |
197 | 'queued_media_file': [unicode], | |
6b9ee0ca | 198 | 'queued_task_id': unicode, |
fa7f9c61 CAW |
199 | |
200 | # A dictionary of logical names to filepaths | |
201 | 'media_files': dict, | |
202 | ||
74ae6b11 CAW |
203 | # The following should be lists of lists, in appropriate file |
204 | # record form | |
6c50c210 CAW |
205 | 'attachment_files': list, |
206 | ||
207 | # If things go badly in processing things, we'll store that | |
208 | # data here | |
209 | 'fail_error': unicode, | |
210 | 'fail_metadata': dict} | |
4d75522b CAW |
211 | |
212 | required_fields = [ | |
b1ae76ae | 213 | 'uploader', 'created', 'media_type', 'slug'] |
4d75522b CAW |
214 | |
215 | default_values = { | |
74ae6b11 CAW |
216 | 'created': datetime.datetime.utcnow, |
217 | 'state': u'unprocessed'} | |
4d75522b | 218 | |
58f96a13 E |
219 | id = MongoPK() |
220 | ||
5ff57582 E |
221 | def media_data_init(self, **kwargs): |
222 | self.media_data.update(kwargs) | |
223 | ||
25bdf9b6 AW |
224 | def get_comments(self, ascending=False): |
225 | if ascending: | |
226 | order = ASCENDING | |
227 | else: | |
228 | order = DESCENDING | |
229 | ||
6f59a3a3 | 230 | return self.db.MediaComment.find({ |
25bdf9b6 | 231 | 'media_entry': self._id}).sort('created', order) |
6f59a3a3 | 232 | |
9c0fe63f CFD |
233 | def url_to_prev(self, urlgen): |
234 | """ | |
235 | Provide a url to the previous entry from this user, if there is one | |
236 | """ | |
25bdf9b6 AW |
237 | cursor = self.db.MediaEntry.find({'_id': {"$gt": self._id}, |
238 | 'uploader': self.uploader, | |
ce2ac488 | 239 | 'state': 'processed'}).sort( |
77b95801 | 240 | '_id', ASCENDING).limit(1) |
25bdf9b6 AW |
241 | for media in cursor: |
242 | return media.url_for_self(urlgen) | |
080a81ec | 243 | |
9c0fe63f CFD |
244 | def url_to_next(self, urlgen): |
245 | """ | |
246 | Provide a url to the next entry from this user, if there is one | |
247 | """ | |
25bdf9b6 AW |
248 | cursor = self.db.MediaEntry.find({'_id': {"$lt": self._id}, |
249 | 'uploader': self.uploader, | |
ce2ac488 | 250 | 'state': 'processed'}).sort( |
77b95801 | 251 | '_id', DESCENDING).limit(1) |
9c0fe63f | 252 | |
25bdf9b6 AW |
253 | for media in cursor: |
254 | return media.url_for_self(urlgen) | |
6ee9c719 | 255 | |
25bdf9b6 AW |
256 | @property |
257 | def get_uploader(self): | |
258 | return self.db.User.find_one({'_id': self.uploader}) | |
25b48323 | 259 | |
b27ec167 | 260 | |
feba5c52 | 261 | class MediaComment(Document, MediaCommentMixin): |
e83dc091 CAW |
262 | """ |
263 | A comment on a MediaEntry. | |
264 | ||
265 | Structure: | |
266 | - media_entry: The media entry this comment is attached to | |
267 | - author: user who posted this comment | |
268 | - created: when the comment was created | |
269 | - content: plaintext (but markdown'able) version of the comment's content. | |
e83dc091 CAW |
270 | """ |
271 | ||
c11f21ab | 272 | __collection__ = 'media_comments' |
25bdf9b6 | 273 | use_dot_notation = True |
6926b23d | 274 | |
c11f21ab JW |
275 | structure = { |
276 | 'media_entry': ObjectId, | |
277 | 'author': ObjectId, | |
278 | 'created': datetime.datetime, | |
279 | 'content': unicode, | |
feba5c52 | 280 | } |
c11f21ab JW |
281 | |
282 | required_fields = [ | |
7bd8197f | 283 | 'media_entry', 'author', 'created', 'content'] |
c11f21ab JW |
284 | |
285 | default_values = { | |
286 | 'created': datetime.datetime.utcnow} | |
287 | ||
288 | def media_entry(self): | |
7bd8197f | 289 | return self.db.MediaEntry.find_one({'_id': self['media_entry']}) |
c11f21ab | 290 | |
25bdf9b6 AW |
291 | @property |
292 | def get_author(self): | |
c11f21ab | 293 | return self.db.User.find_one({'_id': self['author']}) |
6926b23d | 294 | |
c2ddd85e | 295 | |
c11f21ab JW |
296 | REGISTER_MODELS = [ |
297 | MediaEntry, | |
298 | User, | |
299 | MediaComment] | |
d232e0f6 | 300 | |
4329be14 | 301 | |
d232e0f6 CAW |
302 | def register_models(connection): |
303 | """ | |
304 | Register all models in REGISTER_MODELS with this connection. | |
305 | """ | |
db61f7d1 | 306 | connection.register(REGISTER_MODELS) |