]>
Commit | Line | Data |
---|---|---|
b01a1ee3 MT |
1 | #!/usr/bin/python |
2 | ||
53b2117f | 3 | import PIL |
53b2117f | 4 | import io |
3c986f14 | 5 | import ldap |
53b2117f MT |
6 | import logging |
7 | import os.path | |
b01a1ee3 MT |
8 | import tornado.web |
9 | ||
3c986f14 | 10 | from .. import countries |
27714b61 | 11 | from .. import util |
3c986f14 | 12 | |
b01a1ee3 | 13 | from . import base |
beb13102 MT |
14 | from . import ui_modules |
15 | ||
53b2117f MT |
16 | COLOUR_LIGHT = (237,232,232) |
17 | COLOUR_DARK = (49,53,60) | |
18 | ||
da24ac0a | 19 | class IndexHandler(base.BaseHandler): |
beb13102 MT |
20 | @tornado.web.authenticated |
21 | def get(self): | |
22 | results = None | |
23 | ||
24 | # Query Term | |
25 | q = self.get_argument("q", None) | |
26 | ||
27 | # Peform search | |
28 | if q: | |
29 | results = self.backend.accounts.search(q) | |
30 | ||
31 | self.render("users/index.html", q=q, results=results) | |
32 | ||
b01a1ee3 | 33 | |
da24ac0a | 34 | class ShowHandler(base.BaseHandler): |
b01a1ee3 | 35 | @tornado.web.authenticated |
d6c41da2 | 36 | async def get(self, uid): |
b01a1ee3 MT |
37 | account = self.backend.accounts.get_by_uid(uid) |
38 | if not account: | |
39 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
40 | ||
d6c41da2 MT |
41 | # Fetch SIP channels |
42 | sip_channels = await account.get_sip_channels() | |
43 | ||
44 | self.render("users/show.html", account=account, sip_channels=sip_channels) | |
beb13102 MT |
45 | |
46 | ||
53b2117f | 47 | class AvatarHandler(base.BaseHandler): |
14a30250 | 48 | async def get(self, uid): |
e6340233 MT |
49 | if self.browser_accepts("image/webp"): |
50 | format = "WEBP" | |
51 | else: | |
52 | format = "JPEG" | |
53 | ||
53b2117f MT |
54 | # Get the desired size of the avatar file |
55 | size = self.get_argument("size", None) | |
56 | ||
57 | try: | |
58 | size = int(size) | |
59 | except (TypeError, ValueError): | |
60 | size = None | |
61 | ||
62 | logging.debug("Querying for avatar of %s" % uid) | |
63 | ||
64 | # Fetch user account | |
65 | account = self.backend.accounts.get_by_uid(uid) | |
66 | if not account: | |
67 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
68 | ||
69 | # Allow downstream to cache this for a year | |
70 | self.set_expires(31536000) | |
71 | ||
72 | # Resize avatar | |
e6340233 | 73 | avatar = await account.get_avatar(size, format=format) |
53b2117f MT |
74 | |
75 | # If there is no avatar, we serve a default image | |
76 | if not avatar: | |
77 | logging.debug("No avatar uploaded for %s" % account) | |
78 | ||
79 | # Generate a random avatar with only one letter | |
e6340233 | 80 | avatar = await self._get_avatar(account, size=size, format=format) |
53b2117f | 81 | |
c6653ffc MT |
82 | # Deliver the data |
83 | self._deliver_file(avatar, prefix=account.uid) | |
53b2117f | 84 | |
e6340233 | 85 | async def _get_avatar(self, account, size=None, format=None, **args): |
27714b61 | 86 | letters = account.initials |
53b2117f MT |
87 | |
88 | if size is None: | |
89 | size = 256 | |
90 | ||
91 | # The generated avatar cannot be larger than 1024px | |
92 | if size >= 2048: | |
93 | size = 2048 | |
94 | ||
95 | # Cache key | |
e6340233 | 96 | key = "avatar:letter:%s:%s:%s" % ("".join(letters), format or "N/A", size) |
53b2117f MT |
97 | |
98 | # Fetch avatar from the cache | |
e6340233 | 99 | avatar = await self.backend.cache.get(key) |
53b2117f | 100 | if not avatar: |
27714b61 | 101 | avatar = self._make_avatar(letters, size=size, **args) |
53b2117f MT |
102 | |
103 | # Cache for forever | |
14a30250 | 104 | await self.backend.cache.set(key, avatar) |
53b2117f MT |
105 | |
106 | return avatar | |
107 | ||
27714b61 | 108 | def _make_avatar(self, letters, format="PNG", size=None, **args): |
53b2117f MT |
109 | # Load font |
110 | font = PIL.ImageFont.truetype(os.path.join( | |
111 | self.application.settings.get("static_path", ""), | |
112 | "fonts/Prompt-Bold.ttf" | |
113 | ), size, encoding="unic") | |
114 | ||
27714b61 MT |
115 | width = 0 |
116 | height = 0 | |
117 | ||
118 | for letter in letters: | |
119 | # Determine size of the printed letter | |
120 | w, h = font.getsize(letter) | |
121 | ||
122 | # Store the maximum height | |
123 | height = max(h, height) | |
53b2117f | 124 | |
27714b61 MT |
125 | # Add up the width |
126 | width += w | |
127 | ||
128 | # Add the margin | |
129 | width = int(width * 1.5) | |
130 | height = int(height * 1.5) | |
131 | ||
27714b61 MT |
132 | # Generate an image of the correct size |
133 | image = PIL.Image.new("RGBA", (width, height), COLOUR_LIGHT) | |
134 | ||
135 | # Have a canvas | |
136 | draw = PIL.ImageDraw.Draw(image) | |
53b2117f MT |
137 | |
138 | # Draw the letter in the center | |
27714b61 MT |
139 | draw.text((width // 2, height // 2), "".join(letters), |
140 | font=font, anchor="mm", fill=COLOUR_DARK) | |
141 | ||
e6340233 | 142 | return util.generate_thumbnail(image, size, square=True, format=format) |
53b2117f MT |
143 | |
144 | ||
3c986f14 MT |
145 | class EditHandler(base.BaseHandler): |
146 | @tornado.web.authenticated | |
147 | def get(self, uid): | |
148 | account = self.backend.accounts.get_by_uid(uid) | |
149 | if not account: | |
150 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
151 | ||
152 | # Check for permissions | |
153 | if not account.can_be_managed_by(self.current_user): | |
154 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
155 | ||
156 | self.render("users/edit.html", account=account, countries=countries.get_all()) | |
157 | ||
158 | @tornado.web.authenticated | |
14a30250 | 159 | async def post(self, uid): |
3c986f14 MT |
160 | account = self.backend.accounts.get_by_uid(uid) |
161 | if not account: | |
162 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
163 | ||
164 | # Check for permissions | |
165 | if not account.can_be_managed_by(self.current_user): | |
166 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
167 | ||
168 | # Unfortunately this cannot be wrapped into a transaction | |
169 | try: | |
170 | account.first_name = self.get_argument("first_name") | |
171 | account.last_name = self.get_argument("last_name") | |
172 | account.nickname = self.get_argument("nickname", None) | |
173 | account.street = self.get_argument("street", None) | |
174 | account.city = self.get_argument("city", None) | |
175 | account.postal_code = self.get_argument("postal_code", None) | |
076296da | 176 | account.country_code = self.get_argument("country_code") |
3c986f14 MT |
177 | account.description = self.get_argument("description", None) |
178 | ||
179 | # Avatar | |
180 | try: | |
181 | filename, data, mimetype = self.get_file("avatar") | |
182 | ||
183 | if not mimetype.startswith("image/"): | |
184 | raise tornado.web.HTTPError(400, "Avatar is not an image file: %s" % mimetype) | |
185 | ||
77cab7da | 186 | await account.upload_avatar(data) |
3c986f14 MT |
187 | except TypeError: |
188 | pass | |
189 | ||
190 | ||
191 | account.mail_routing_address = self.get_argument("mail_routing_address", None) | |
192 | ||
193 | # Telephone | |
194 | account.phone_numbers = self.get_argument("phone_numbers", "").splitlines() | |
195 | account.sip_routing_address = self.get_argument("sip_routing_address", None) | |
196 | except ldap.STRONG_AUTH_REQUIRED as e: | |
197 | raise tornado.web.HTTPError(403, "%s" % e) from e | |
198 | ||
199 | # Redirect back to user page | |
200 | self.redirect("/users/%s" % account.uid) | |
201 | ||
202 | ||
1cd4d7d3 MT |
203 | class DeleteHandler(base.BaseHandler): |
204 | @tornado.web.authenticated | |
205 | def get(self, uid): | |
206 | account = self.backend.accounts.get_by_uid(uid) | |
207 | if not account: | |
208 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
209 | ||
210 | # Check for permissions | |
211 | if not account.can_be_deleted_by(self.current_user): | |
212 | raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account)) | |
213 | ||
214 | self.render("users/delete.html", account=account) | |
215 | ||
216 | @tornado.web.authenticated | |
217 | async def post(self, uid): | |
218 | account = self.backend.accounts.get_by_uid(uid) | |
219 | if not account: | |
220 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
221 | ||
222 | # Check for permissions | |
223 | if not account.can_be_deleted_by(self.current_user): | |
224 | raise tornado.web.HTTPError(403, "%s cannot delete %s" % (self.current_user, account)) | |
225 | ||
226 | # Delete! | |
227 | with self.db.transaction(): | |
228 | await account.delete(self.current_user) | |
229 | ||
230 | self.render("users/deleted.html", account=account) | |
231 | ||
232 | ||
e4d2f51f MT |
233 | class PasswdHandler(base.BaseHandler): |
234 | @tornado.web.authenticated | |
235 | def get(self, uid): | |
236 | account = self.backend.accounts.get_by_uid(uid) | |
237 | if not account: | |
238 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
239 | ||
240 | # Check for permissions | |
241 | if not account.can_be_managed_by(self.current_user): | |
242 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
243 | ||
244 | self.render("users/passwd.html", account=account) | |
245 | ||
246 | @tornado.web.authenticated | |
247 | def post(self, uid): | |
248 | account = self.backend.accounts.get_by_uid(uid) | |
249 | if not account: | |
250 | raise tornado.web.HTTPError(404, "Could not find account %s" % uid) | |
251 | ||
252 | # Check for permissions | |
253 | if not account.can_be_managed_by(self.current_user): | |
254 | raise tornado.web.HTTPError(403, "%s cannot manage %s" % (self.current_user, account)) | |
255 | ||
256 | # Get current password | |
257 | password = self.get_argument("password") | |
258 | ||
259 | # Get new password | |
260 | password1 = self.get_argument("password1") | |
261 | password2 = self.get_argument("password2") | |
262 | ||
263 | # Passwords must match | |
264 | if not password1 == password2: | |
265 | raise tornado.web.HTTPError(400, "Passwords do not match") | |
266 | ||
267 | # XXX Check password complexity | |
268 | ||
269 | # Check if old password matches | |
270 | if not account.check_password(password): | |
271 | raise tornado.web.HTTPError(403, "Incorrect password for %s" % account) | |
272 | ||
273 | # Save new password | |
274 | account.passwd(password1) | |
275 | ||
276 | # Redirect back to user's page | |
277 | self.redirect("/users/%s" % account.uid) | |
278 | ||
279 | ||
da24ac0a | 280 | class GroupIndexHandler(base.BaseHandler): |
f39251eb MT |
281 | @tornado.web.authenticated |
282 | def get(self): | |
283 | # Only staff can see other groups | |
284 | if not self.current_user.is_staff(): | |
285 | raise tornado.web.HTTPError(403) | |
286 | ||
287 | self.render("users/groups/index.html") | |
288 | ||
289 | ||
da24ac0a | 290 | class GroupShowHandler(base.BaseHandler): |
f39251eb MT |
291 | @tornado.web.authenticated |
292 | def get(self, gid): | |
293 | # Only staff can see other groups | |
294 | if not self.current_user.is_staff(): | |
295 | raise tornado.web.HTTPError(403) | |
296 | ||
297 | # Fetch group | |
298 | group = self.backend.groups.get_by_gid(gid) | |
299 | if not group: | |
300 | raise tornado.web.HTTPError(404, "Could not find group %s" % gid) | |
301 | ||
302 | self.render("users/groups/show.html", group=group) | |
303 | ||
304 | ||
bb440bad MT |
305 | class SubscribeHandler(base.BaseHandler): |
306 | @tornado.web.authenticated | |
307 | def get(self): | |
308 | self.render("users/subscribe.html") | |
309 | ||
310 | @tornado.web.authenticated | |
311 | def post(self): | |
312 | # Give consent | |
313 | with self.db.transaction(): | |
314 | self.current_user.consents_to_promotional_emails = True | |
315 | ||
316 | self.render("users/subscribed.html") | |
317 | ||
318 | ||
319 | class UnsubscribeHandler(base.BaseHandler): | |
320 | @tornado.web.authenticated | |
321 | def get(self): | |
322 | if self.current_user.consents_to_promotional_emails: | |
323 | return self.render("users/unsubscribe.html") | |
324 | ||
325 | self.render("users/unsubscribed.html") | |
326 | ||
327 | @tornado.web.authenticated | |
328 | def post(self): | |
329 | # Withdraw consent | |
330 | with self.db.transaction(): | |
331 | self.current_user.consents_to_promotional_emails = False | |
332 | ||
333 | self.render("users/unsubscribed.html") | |
334 | ||
335 | ||
beb13102 MT |
336 | class ListModule(ui_modules.UIModule): |
337 | def render(self, accounts, show_created_at=False): | |
338 | return self.render_string("users/modules/list.html", accounts=accounts, | |
339 | show_created_at=show_created_at) |