Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Imports 

2 

3from django.conf import settings 

4from django.db import models 

5from django.utils.translation import ugettext_lazy as _ 

6import hashlib 

7 

8# Exports 

9 

10__all__ = ( 

11 "ProfileMixin", 

12 "ProfileModel", 

13 "ProfileSection", 

14 "ProfileSectionItem", 

15) 

16 

17# Constants 

18 

19AUTH_USER_MODEL = settings.AUTH_USER_MODEL 

20 

21LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL") 

22 

23STATIC_URL = getattr(settings, "STATIC_URL") 

24 

25# Descriptors 

26 

27 

28class ProfileSection(object): 

29 """Collects related profile fields together for display.""" 

30 

31 def __init__(self, title): 

32 self.items = list() 

33 self.title = title 

34 

35 def add_item(self, label, value): 

36 """Add an item to the section. 

37 

38 :param label: The label for the item, i.e. field's verbose name. 

39 :type label: str 

40 

41 :param value: The value of the item. 

42 

43 :rtype: ProfileSectionItem 

44 

45 """ 

46 item = ProfileSectionItem(label, value) 

47 self.items.append(item) 

48 

49 return item 

50 

51 

52class ProfileSectionItem(object): 

53 """An individual field within a section.""" 

54 

55 def __init__(self, label, value): 

56 self.label = label 

57 self.value = value 

58 

59# Mixins 

60 

61 

62class ProfileMixin(object): 

63 """Establishes the API for user profiles.""" 

64 

65 # class Meta: 

66 # abstract = True 

67 

68 def __str__(self): 

69 return self.get_full_name() 

70 

71 @property 

72 def date_joined(self): 

73 # noinspection PyUnresolvedReferences 

74 return self.user.date_joined 

75 

76 @property 

77 def email(self): 

78 """Aliased for the user's email address.""" 

79 # noinspection PyUnresolvedReferences 

80 return self.user.email 

81 

82 @classmethod 

83 def for_user(cls, user, create=False, profile_name="profile"): 

84 """Get (or create) the profile for a given user. 

85 

86 :param user: The user. 

87 :type user: AUTH_USER_MODEL 

88 

89 :param create: Indicates whether a profile should be created if one does not already exist. 

90 :type create: bool 

91 

92 :param profile_name: The related name of the profile field. 

93 :type profile_name: str 

94 

95 :rtype: instance 

96 :returns: An instance of a profile model for the user. 

97 

98 """ 

99 # There's no use in trying if the user is anonymous. 

100 if not user.is_authenticated: 

101 return None 

102 

103 # It is possible that the profile is already available on the user object. Try that before executing the rest of 

104 # the code. But does this actually save a query? 

105 # noinspection PyUnresolvedReferences 

106 try: 

107 return getattr(user, profile_name) 

108 except (AttributeError, cls.DoesNotExist): 

109 pass 

110 

111 # noinspection PyUnresolvedReferences 

112 try: 

113 # noinspection PyUnresolvedReferences 

114 return cls.objects.get(user=user) 

115 except cls.DoesNotExist: 

116 pass 

117 

118 if create: 

119 # noinspection PyUnresolvedReferences 

120 profile, created = cls.objects.get_or_create(user=user) 

121 return profile 

122 

123 return None 

124 

125 @property 

126 def full_name(self): 

127 """Aliased for ``get_full_name()``.""" 

128 return self.get_full_name() 

129 

130 @property 

131 def full_name_with_honorifics(self): 

132 """Alias of ``get_full_name_with_honorifics()``.""" 

133 return self.get_full_name_with_honorifics() 

134 

135 def get_form_class(self, request=None): 

136 """Get the form class to use for editing the profile. 

137 

138 :param request: The current request instance. 

139 :type request: HttpRequest 

140 

141 """ 

142 raise NotImplementedError() 

143 

144 def get_full_name(self): 

145 """Get the full name of the user, or the user name if a full name is not available. 

146 

147 :rtype: str 

148 

149 """ 

150 raise NotImplementedError() 

151 

152 def get_full_name_with_honorifics(self): 

153 """Get the full name of the user, including prefix and suffix, if available. 

154 

155 :rtype: str 

156 

157 """ 

158 raise NotImplementedError() 

159 

160 # noinspection PyMethodMayBeStatic 

161 def get_redirect_url(self): 

162 """Get the URL to which the user prefers to be redirected after logging in. 

163 

164 :rtype: str 

165 

166 .. note:: 

167 By default, the ``settings.LOGIN_REDIRECT_URL`` is returned. The profile model may implement a user 

168 preference for the redirect. 

169 

170 """ 

171 return LOGIN_REDIRECT_URL 

172 

173 def get_thumbnail(self): 

174 """Get the thumbnail URL for the user's avatar. 

175 

176 :rtype: str 

177 

178 """ 

179 return self._get_gravatar_url() 

180 

181 def get_sections(self): 

182 """Get the sections to display for profile detail. 

183 

184 :rtype: list[ProfileSection] 

185 

186 """ 

187 raise NotImplementedError() 

188 

189 # noinspection PyMethodMayBeStatic 

190 def get_time_zone(self): 

191 """Get the timezone of the user. If unavailable, the default time zone should be returned. 

192 

193 :rtype: str 

194 

195 .. note:: 

196 The profile model that uses this mixin should implement a field (or other means) of establishing the user's 

197 time zone (using geo-location for example), defaulting to ``settings.TIME_ZONE`` only if a time zone 

198 preference is unavailable. 

199 

200 """ 

201 return settings.TIME_ZONE 

202 

203 @property 

204 def has_thumbnail(self): 

205 """Indicates the profile has a thumbnail image. 

206  

207 :rtype bool 

208  

209 """ 

210 return True 

211 

212 @property 

213 def last_login(self): 

214 """Alias for the user's last login.""" 

215 # noinspection PyUnresolvedReferences 

216 return self.user.last_login 

217 

218 @property 

219 def redirect_url(self): 

220 """Alias for ``get_redirect_url()``.""" 

221 return self.get_redirect_url() 

222 

223 @property 

224 def sections(self): 

225 """Alias for ``get_sections()``.""" 

226 return self.get_sections() 

227 

228 @property 

229 def thumbnail(self): 

230 """Aliased for ``get_thumbnail()``.""" 

231 return self.get_thumbnail() 

232 

233 @property 

234 def username(self): 

235 """Aliased for the user's ``username``.""" 

236 # noinspection PyUnresolvedReferences 

237 return self.user.username 

238 

239 def _get_gravatar_url(self): 

240 """Get the user's gravatar.com URL. 

241 

242 :rtype: str 

243 

244 """ 

245 encoded_email = self.email.strip().lower().encode("utf-8") 

246 email_hash = hashlib.md5(encoded_email).hexdigest() 

247 

248 # IDEA: The gavatar could be cached locally to save a URL call. 

249 return "https://secure.gravatar.com/avatar/%s.jpg" % email_hash 

250 

251 

252class ProfileModel(ProfileMixin, models.Model): 

253 """An implementation of the profile mixin with sensible defaults. 

254 

255 .. note:: 

256 No fields with foreign keys are included. 

257 

258 This model extends the mixin to establishe some common profile attributes: 

259 

260 .. code-block:: python 

261 

262 from superdjango.contrib.accounts.profiles.models import ProfileModel 

263 

264 class Profile(ProfileModel): 

265 

266 user = models.OneToOneField( 

267 AUTH_USER_MODEL, 

268 help_text=_("The user account to which this profile is attached."), 

269 related_name="profile", 

270 verbose_name=_("user") 

271 ) 

272 

273 class Meta: 

274 verbose_name = _("User Profile") 

275 verbose_name_plural = _("User Profiles") 

276 

277 """ 

278 

279 address = models.TextField( 

280 _("address"), 

281 blank=True, 

282 help_text=_("Enter your mailing address."), 

283 null=True 

284 ) 

285 

286 first_name = models.CharField( 

287 _("first name"), 

288 blank=True, 

289 help_text=_("Your given name."), 

290 max_length=128 

291 ) 

292 

293 gravatar_enabled = models.BooleanField( 

294 _("enable gravatar"), 

295 default=True, 

296 help_text=_("Use gravatar.com for your profile picture.") 

297 ) 

298 

299 last_name = models.CharField( 

300 _("last name"), 

301 blank=True, 

302 help_text=_("Your family name."), 

303 max_length=128 

304 ) 

305 

306 middle_name = models.CharField( 

307 _("middle name"), 

308 blank=True, 

309 help_text=_("Your middle name or initial."), 

310 max_length=128, 

311 null=True 

312 ) 

313 

314 phone_number = models.CharField( 

315 _("phone number"), 

316 blank=True, 

317 help_text=_("Enter your phone number."), 

318 max_length=128 

319 ) 

320 

321 photo = models.FileField( 

322 _("photo"), 

323 blank=True, 

324 help_text=_("Upload a photo to use for your profile picture."), 

325 null=True 

326 ) 

327 

328 prefix = models.CharField( 

329 _("prefix"), 

330 blank=True, 

331 help_text=_("Enter a prefix to appear before your name or leave blank for nothing."), 

332 max_length=64 

333 ) 

334 

335 suffix = models.CharField( 

336 _("suffix"), 

337 blank=True, 

338 help_text=_("Enter a suffix to appear after your name or leave blank for nothing."), 

339 max_length=64 

340 ) 

341 

342 # redirect_to choices must be established by the project. 

343 redirect_to = models.CharField( 

344 _("redirect to"), 

345 blank=True, 

346 help_text=_("By default, go to this page after logging in."), 

347 max_length=256, 

348 ) 

349 

350 # time_zone choices must be established by the project. 

351 time_zone = models.CharField( 

352 _("time zone"), 

353 blank=True, 

354 help_text=_("Select the timezone in which you reside or work."), 

355 max_length=128 

356 ) 

357 

358 class Meta: 

359 abstract = True 

360 

361 def get_form_class(self, request=None): 

362 from .forms import ProfileForm 

363 return ProfileForm 

364 

365 def get_full_name(self): 

366 a = list() 

367 a.append(self.first_name) 

368 

369 if self.middle_name: 

370 a.append(self.middle_name) 

371 

372 a.append(self.last_name) 

373 

374 if len(a) == 0: 

375 a.append(self.username) 

376 

377 return " ".join(a) 

378 

379 def get_full_name_with_honorifics(self): 

380 a = list() 

381 

382 if self.prefix: 

383 a.append(self.prefix) 

384 

385 a.append(self.get_full_name()) 

386 

387 if self.suffix: 

388 a.append(self.suffix) 

389 

390 return " ".join(a) 

391 

392 def get_redirect_url(self): 

393 """Get the user's redirect choice or the ``LOGIN_REDIRECT_URL`` if no choice has been made. 

394 

395 :rtype: str 

396 

397 """ 

398 return self.redirect_to or LOGIN_REDIRECT_URL 

399 

400 def get_sections(self): 

401 """Get profile sections.""" 

402 a = list() 

403 

404 contact = ProfileSection(_("Contact")) 

405 contact.add_item(_("Name"), self.get_full_name_with_honorifics()) 

406 contact.add_item(_("Email"), self.email) 

407 contact.add_item(_("Phone"), self.phone_number) 

408 contact.add_item(_("Address"), self.address) 

409 a.append(contact) 

410 

411 user = ProfileSection(_("User")) 

412 user.add_item(_("User Name"), self.username) 

413 user.add_item(_("Since"), self.date_joined) 

414 a.append(user) 

415 

416 prefs = ProfileSection(_("Preferences")) 

417 prefs.add_item(_("Redirect To"), self.get_redirect_url()) 

418 prefs.add_item(_("Gravatar Enabled"), _("Yes") if self.gravatar_enabled else _("No")) 

419 prefs.add_item(_("Time Zone"), self.get_time_zone()) 

420 a.append(prefs) 

421 

422 return a 

423 

424 def get_thumbnail(self): 

425 """Uses gravatar or an uploaded photo.""" 

426 if self.gravatar_enabled: 

427 return self._get_gravatar_url() 

428 

429 return self.photo.url 

430 

431 def get_time_zone(self): 

432 """Get the time zone of the user or the default timezone. 

433 

434 :rtype: str 

435 

436 """ 

437 return self.time_zone or settings.TIME_ZONE 

438 

439 @property 

440 def has_thumbnail(self): 

441 """Determines a gravatar or photo is available.""" 

442 if self.gravatar_enabled: 

443 return True 

444 

445 if self.photo: 

446 return True 

447 

448 return False 

449 

450 @property 

451 def title(self): 

452 """Automated object title for UI views.""" 

453 return self.full_name