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.contrib.contenttypes.models import ContentType 

5from django.core.exceptions import ObjectDoesNotExist 

6from django.db import models 

7from django.utils.encoding import force_str 

8from django.utils.translation import gettext_lazy as _ 

9from superdjango.conf import SUPERDJANGO 

10from superdjango.shortcuts import get_user_name 

11from .constants import CREATE_VERBS, DELETE_VERBS, DETAIL_VERBS, UPDATE_VERBS 

12 

13# Exports 

14 

15__all__ = ( 

16 "HistoryModel", 

17) 

18 

19# Constants 

20 

21AUTH_USER_MODEL = settings.AUTH_USER_MODEL 

22 

23# Models 

24 

25 

26class HistoryModel(models.Model): 

27 """An abstract model for implementing a running record history. 

28 

29 .. code-block:: python 

30 

31 from superdjango.db.history.models import HistoryModel 

32 

33 class LogEntry(HistoryModel): 

34 

35 class Meta: 

36 get_latest_by = "added_dt" 

37 ordering = ["-added_dt"] 

38 verbose_name = _("Log Entry") 

39 verbose_name_plural = _("Log Entries") 

40 

41 """ 

42 

43 absolute_url = models.CharField( 

44 _("URL"), 

45 blank=True, 

46 help_text=_("The URL of the model instance."), 

47 max_length=1024, 

48 null=True 

49 ) 

50 

51 added_dt = models.DateTimeField( 

52 _("added date/time"), 

53 auto_now_add=True, 

54 help_text=_("Date and time the action was taken.") 

55 ) 

56 

57 content_type = models.ForeignKey( 

58 ContentType, 

59 help_text=_("The internal content type of the object."), 

60 on_delete=models.CASCADE, 

61 related_name="%(app_label)s_%(class)s_record_history", 

62 verbose_name=_('content type') 

63 ) 

64 

65 object_id = models.CharField( 

66 _('object id'), 

67 help_text=_("The object (record) ID."), 

68 max_length=256 

69 ) 

70 

71 object_label = models.CharField( 

72 _("object label"), 

73 blank=True, 

74 help_text=_("The string representation of the object, i.e. it's title, label, etc."), 

75 max_length=128 

76 ) 

77 

78 user = models.ForeignKey( 

79 AUTH_USER_MODEL, 

80 blank=True, 

81 help_text=_("The user that performed the action."), 

82 null=True, 

83 on_delete=models.SET_NULL, 

84 related_name="%(app_label)s_%(class)s_record_history", 

85 verbose_name=_("user") 

86 ) 

87 

88 user_name = models.CharField( 

89 _("user name"), 

90 blank=True, 

91 help_text=_("The name of the user that performed the action."), 

92 max_length=128 

93 ) 

94 

95 # create, delete, detail (view), update, or other custom verbs. No choices are defined to allow custom verbs. 

96 CREATE = "create" 

97 DELETE = "delete" 

98 DETAIL = "detail" 

99 UPDATE = "update" 

100 verb = models.CharField( 

101 _("verb"), 

102 help_text=_("The verb (action) taken on a record."), 

103 max_length=128 

104 ) 

105 

106 verb_display = models.CharField( 

107 _("verb display"), 

108 blank=True, 

109 help_text=_("The display value of the verb."), 

110 max_length=128, 

111 null=True 

112 ) 

113 

114 verbose_name = models.CharField( 

115 _("verbose name"), 

116 blank=True, 

117 help_text=_("The verbose name of the model."), 

118 max_length=256 

119 ) 

120 

121 class Meta: 

122 abstract = True 

123 

124 def __str__(self): 

125 return self.get_message() 

126 

127 @property 

128 def action(self): 

129 """An alias for ``get_verb_display()``.""" 

130 return self.get_verb_display() 

131 

132 def get_message(self): 

133 """Get the message that describes the action. 

134 

135 :rtype: str 

136 

137 .. note:: 

138 The added date/time of the entry is *not* localized for the current user's timezone. To support this, you'll 

139 need to implement the message in a template. 

140 

141 """ 

142 template = '%(user_name)s %(verb)s "%(object_label)s" %(verbose_name)s at %(added_dt)s.' 

143 

144 # noinspection PyUnresolvedReferences 

145 added_dt = self.added_dt.strftime(SUPERDJANGO.DATETIME_MASK) 

146 

147 # noinspection PyUnresolvedReferences 

148 verbose_name = self.verbose_name.lower() 

149 

150 context = { 

151 'added_dt': added_dt, 

152 'object_label': self.object_label, 

153 'user_name': self.performed_by, 

154 'verb': self.get_verb_display(), 

155 'verbose_name': verbose_name, 

156 } 

157 

158 return _(template % context) 

159 

160 def get_object(self): 

161 """Get the model instance that was the subject of the entry. 

162 

163 :returns: The model instance. If the object was deleted, ``None`` is returned. 

164 

165 """ 

166 try: 

167 # noinspection PyUnresolvedReferences 

168 return self.content_type.get_object_for_this_type(pk=self.object_id) 

169 except ObjectDoesNotExist: 

170 return None 

171 

172 def get_verb_display(self): 

173 """Get the human-friendly verb. 

174 

175 :rtype: str 

176 

177 """ 

178 if self.verb_display: 

179 return self.verb_display 

180 elif self.is_create: 

181 return "added" 

182 elif self.is_delete: 

183 return "removed" 

184 elif self.is_detail: 

185 return "viewed" 

186 elif self.is_update: 

187 return "updated" 

188 else: 

189 return self.verb 

190 

191 def get_url(self): 

192 """Get the URL of the model instance. 

193 

194 The URL may not be available. Additionally, if the action represents a delete, no URL is returned. 

195 

196 :rtype: str | None 

197 

198 """ 

199 if self.is_delete: 

200 return None 

201 

202 return self.absolute_url 

203 

204 @property 

205 def is_create(self): 

206 """Indicates the action is an addition. 

207 

208 :rtype: bool 

209 

210 """ 

211 return self.verb in CREATE_VERBS 

212 

213 @property 

214 def is_delete(self): 

215 """Indicates the action is a delete. 

216 

217 :rtype: bool 

218 

219 """ 

220 return self.verb in DELETE_VERBS 

221 

222 @property 

223 def is_detail(self): 

224 """Indicates the action is a detail. 

225 

226 :rtype: bool 

227 

228 """ 

229 return self.verb in DETAIL_VERBS 

230 

231 @property 

232 def is_update(self): 

233 """Indicates the action was an update. 

234 

235 :rtype: bool 

236 

237 """ 

238 return self.verb in UPDATE_VERBS 

239 

240 @classmethod 

241 def log(cls, record, user, verb, fields=None, url=None, verb_display=None): 

242 """Create a new history entry. 

243 

244 :param record: The model instance. 

245 

246 :param user: The user (instance) performing the action. 

247 

248 :param verb: The action taken. 

249 :type verb: str 

250 

251 :param fields: A list of changed fields. 

252 :type fields: list[superdjango.db.history.utils.FieldChange] 

253 

254 :param url: The URL of the model instance. Typically that of the detail view. If omitted, an attempt will be 

255 made to acquire the URL from ``get_absolute_url()``. 

256 :type url: str 

257 

258 :param verb_display: The human0friendly name of the action taken. 

259 :type verb_display: str 

260 

261 :returns: The log entry instance. 

262 

263 .. note:: 

264 By default, nothing is done with ``fields``. When you extend the class, you may save the fields to the 

265 extending model, or iterate over them to save each change to a separate model that refers back to the new 

266 history instance. See ``log_field_changes()``. 

267 

268 """ 

269 if hasattr(record, "get_display_name") and callable(record.get_display_name): 

270 object_label = record.get_display_name() 

271 else: 

272 object_label = force_str(str(record)) 

273 

274 # noinspection PyProtectedMember 

275 verbose_name = record._meta.verbose_name 

276 

277 if url is None and verb not in DELETE_VERBS: 

278 try: 

279 url = record.get_absolute_url() 

280 except AttributeError: 

281 pass 

282 

283 kwargs = { 

284 'absolute_url': url, 

285 'content_type': ContentType.objects.get_for_model(record), 

286 'object_id': record.pk, 

287 'object_label': object_label, 

288 'user': user, 

289 'user_name': get_user_name(user), 

290 'verb': verb, 

291 'verb_display': verb_display, 

292 'verbose_name': verbose_name, 

293 } 

294 

295 # noinspection PyUnresolvedReferences 

296 history = cls(**kwargs) 

297 history.save() 

298 

299 cls.log_field_changes(history, verb, fields=fields) 

300 

301 return history 

302 

303 @classmethod 

304 def log_field_changes(cls, instance, verb, fields=None): 

305 """Log changes to fields. 

306 

307 :param instance: The history record, NOT the original model instance. 

308 

309 :param verb: The action taken. This allows verbs such as create or delete to be ignored. 

310 :type verb: str 

311 

312 :param fields: A list of changed fields. 

313 :type fields: list[superdjango.db.history.utils.FieldChange] 

314 

315 """ 

316 pass 

317 

318 @property 

319 def message(self): 

320 """An alias for ``get_message()``.""" 

321 return self.get_message() 

322 

323 @property 

324 def performed_by(self): 

325 """Get the name of the user that performed the action. 

326 

327 :rtype: str 

328 

329 """ 

330 # noinspection PyUnresolvedReferences 

331 if self.user_id: 

332 return get_user_name(self.user) 

333 

334 return self.user_name