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""" 

2Library of shortcuts. 

3 

4""" 

5 

6# Imports 

7 

8from django.apps import apps as django_apps 

9from django.conf import settings 

10from django.contrib.staticfiles import finders 

11from django.core.exceptions import ImproperlyConfigured 

12from django.db.models import AutoField 

13from django.template import Context, loader, Template 

14from django.template.exceptions import TemplateDoesNotExist 

15from django.utils.module_loading import module_has_submodule 

16from importlib import import_module 

17from itertools import chain 

18from operator import attrgetter 

19from superdjango.conf import SUPERDJANGO 

20import warnings 

21 

22# Exports 

23 

24__all__ = ( 

25 "copy_model_instance", 

26 "get_app_modules", 

27 "get_model", 

28 "get_setting", 

29 "get_setting_with_prefix", 

30 "get_user_name", 

31 "has_o2o", 

32 "parse_string", 

33 "parse_template", 

34 "static_file_exists", 

35 "template_exists", 

36 "there_can_be_only_one", 

37 "title", 

38 "user_in_group", 

39 # "CombinedQuerySet", 

40 "CompareQuerySet", 

41) 

42 

43# Constants 

44 

45DEBUG = getattr(settings, "DEBUG", False) 

46 

47# Functions 

48 

49 

50def copy_model_instance(instance): 

51 """Copy an instance of a model. 

52 

53 :param instance: The model instance to be copied. 

54 :type instance: Model 

55 

56 :returns: Returns a copy of the model instance. 

57 

58 .. tip:: 

59 The new instance has *not* been saved to the database. Also, reference fields must be manually recreated using 

60 the original instance. 

61 

62 """ 

63 initial_values = dict() 

64 

65 # noinspection PyProtectedMember 

66 for f in instance._meta.fields: 

67 # noinspection PyProtectedMember 

68 if not isinstance(f, AutoField) and f not in list(instance._meta.parents.values()): 

69 initial_values[f.name] = getattr(instance, f.name) 

70 

71 return instance.__class__(**initial_values) 

72 

73 

74def get_app_modules(name): 

75 """Yields tuples of ``(app_name, module)`` for each installed app that includes the given module name. 

76 

77 :param name: The module name. 

78 :type name: str 

79 

80 """ 

81 for app in django_apps.get_app_configs(): 

82 if module_has_submodule(app.module, name): 

83 yield app.name, import_module("%s.%s" % (app.name, name)) 

84 

85 

86def get_model(dotted_path, raise_exception=True): 

87 """Get the model for the given dotted path. 

88 

89 :param dotted_path: The ``app_label.ModelName`` path of the model to return. 

90 :type dotted_path: str 

91 

92 :param raise_exception: Raise an exception on failure. 

93 :type raise_exception: bool 

94 

95 """ 

96 try: 

97 return django_apps.get_model(dotted_path, require_ready=False) 

98 except ValueError: 

99 if raise_exception: 

100 raise ImproperlyConfigured('dotted_path must be in the form of "app_label.ModelName".') 

101 except LookupError: 

102 if raise_exception: 

103 raise ImproperlyConfigured("dotted_path refers to a model that has not been installed: %s" % dotted_path) 

104 

105 return None 

106 

107 

108def get_setting(name, default=None): 

109 """Get the value of the named setting from the ``settings.py`` file. 

110 

111 :param name: The name of the setting. 

112 :type name: str 

113 

114 :param default: The default value. 

115 

116 """ 

117 message = "get_settings() will likely be removed in the next minor version. Use " \ 

118 "conf.SUPERDJANGO.get() instead." 

119 warnings.warn(message, PendingDeprecationWarning) 

120 

121 return getattr(settings, name.upper(), default) 

122 

123 

124def get_setting_with_prefix(name, default=None, prefix="SUPERDJANGO"): 

125 """Get the value of the named setting from the ``settings.py`` file. 

126 

127 :param name: The name of the setting. 

128 :type name: str 

129 

130 :param default: The default value. 

131 

132 :param prefix: The setting prefix. 

133 :type prefix: str 

134 

135 """ 

136 message = "get_setting_with_prefix() will likely be removed in the next minor version. Use " \ 

137 "conf.SUPERDJANGO.get() instead." 

138 warnings.warn(message, PendingDeprecationWarning) 

139 

140 full_name = "%s_%s" % (prefix, name.upper()) 

141 return get_setting(full_name, default=default) 

142 

143 

144def get_user_name(user): 

145 """Get the full name of the user, if available, or just the value of the username field if not. 

146 

147 :param user: The user instance. 

148 

149 :rtype: str | None 

150 

151 :raises: ``AttributeError`` if a custom user model has been implemented without a ``get_full_name()`` method. 

152 

153 .. tip:: 

154 It is safe to call this function at runtime where user is ``None``. 

155 

156 """ 

157 if user is None: 

158 return None 

159 

160 full_name = user.get_full_name() 

161 if len(full_name) > 0: 

162 return full_name 

163 

164 return getattr(user, user.USERNAME_FIELD) 

165 

166 

167def has_o2o(instance, field_name): 

168 """Test whether a OneToOneField is populated. 

169 

170 :param instance: The model instance to check. 

171 :type instance: object 

172 

173 :param field_name: The name of the one to one field. 

174 :type field_name: str || unicode 

175 

176 :rtype: bool 

177 

178 This is the same `hasattr()``, but is more explicit and easier to remember. 

179 

180 See http://devdocs.io/django/topics/db/examples/one_to_one 

181 

182 """ 

183 return hasattr(instance, field_name) 

184 

185 

186def parse_string(string, context): 

187 """Parse a string as a Django template. 

188 

189 :param string: The name of the template. 

190 :type string: str 

191 

192 :param context: Context variables. 

193 :type context: dict 

194 

195 :rtype: str 

196 

197 """ 

198 message = "parse_string() has been moved to superdjango.html.shortcuts and will be removed in the next " \ 

199 "minor version." 

200 warnings.warn(message, PendingDeprecationWarning) 

201 

202 template = Template(string) 

203 return template.render(Context(context)) 

204 

205 

206def parse_template(template, context): 

207 """Ad hoc means of parsing a template using Django's built-in loader. 

208 

209 :param template: The name of the template. 

210 :type template: str || unicode 

211 

212 :param context: Context variables. 

213 :type context: dict 

214 

215 :rtype: str 

216 

217 """ 

218 message = "parse_template() has been moved to superdjango.html.shortcuts and will be removed in the next " \ 

219 "minor version." 

220 warnings.warn(message, PendingDeprecationWarning) 

221 

222 return loader.render_to_string(template, context) 

223 

224 

225def static_file_exists(path, tokens=None): 

226 """Determine whether a static file exists. 

227 

228 :param path: The path to be checked. 

229 :type path: str 

230 

231 :param tokens: If given, format will be used to replace the tokens in the path. 

232 :type tokens: dict 

233 

234 :rtype: bool 

235 

236 """ 

237 if tokens is not None: 

238 path = path.format(**tokens) 

239 

240 return finders.find(path) is not None 

241 

242 

243def template_exists(name): 

244 """Indicates whether the given template exists. 

245 

246 :param name: The name (path) of the template to load. 

247 :type name: str 

248 

249 :rtype: bool 

250 

251 """ 

252 message = "template_exists() has been moved to superdjango.html.shortcuts and will be removed in the next " \ 

253 "minor version." 

254 warnings.warn(message, PendingDeprecationWarning) 

255 

256 try: 

257 loader.get_template(name) 

258 return True 

259 except TemplateDoesNotExist: 

260 return False 

261 

262 

263def there_can_be_only_one(cls, instance, field_name): 

264 """Helper function that ensures a given boolean field is ``True`` for only one record in the database. It is 

265 intended for use with ``pre_save`` signals. 

266 

267 :param cls: The class (sender) emitting the signal. 

268 :type cls: class 

269 

270 :param instance: The instance to be checked. 

271 :type: instance: object 

272 

273 :param field_name: The name of the field to be checked. Must be a ``BooleanField``. 

274 :type field_name: str | unicode 

275 

276 :raises: ``AttributeError`` if the ``field_name`` does not exist on the ``instance``. 

277 

278 """ 

279 field_value = getattr(instance, field_name) 

280 

281 if field_value: 

282 cls.objects.update(**{field_name: False}) 

283 

284 

285def title(value, uppers=None): 

286 """Smart title conversion. 

287 

288 :param value: The value to converted to Title Case. 

289 :type value: str 

290 

291 :param uppers: A list of keywords that are alway upper case. 

292 :type uppers: list[str] 

293 

294 :rtype: str 

295 

296 """ 

297 if uppers is None: 

298 uppers = list() 

299 

300 uppers += SUPERDJANGO.UPPERS 

301 

302 tokens = value.split(" ") 

303 _value = list() 

304 for t in tokens: 

305 if t.lower() in uppers: 

306 v = t.upper() 

307 else: 

308 v = t.title() 

309 

310 _value.append(v) 

311 

312 return " ".join(_value) 

313 

314 

315def user_in_group(user, group): 

316 """Determine whether a given user is in a particular group. 

317 

318 :param user: The user instance to be checked. 

319 :type user: User 

320 

321 :param group: The name of the group. 

322 :type group: str 

323 

324 :rtype: bool 

325 

326 """ 

327 return user.groups.filter(name=group).exists() 

328 

329 

330# Classes 

331 

332 

333# TODO: Original ideas for CombinedQuerySet doesn't really work. Needs lots of thought and testing to mimic a queryset. 

334# Will work on this again when I have a need for a combined qs. 

335# class CombinedQuerySet(object): 

336# """Combines two are more querysets as though they were one.""" 

337# 

338# def __init__(self, querysets=None): 

339# if querysets is not None: 

340# self.rows = querysets 

341# else: 

342# self.rows = list() 

343# 

344# if querysets is not None: 

345# self.results = list(chain(*querysets)) 

346# else: 

347# self.results = list() 

348# 

349# def append(self, qs): 

350# self.results.append(chain(qs)) 

351# 

352# def filter(self, **criteria): 

353# _results = list() 

354# for qs in self.results: 

355# new_qs = qs.filter(**criteria) 

356# _results.append(new_qs) 

357# 

358# return CombinedQuerySet(querysets=_results) 

359# 

360# def order_by(self, field_name): 

361# return sorted(chain(*self.results), key=attrgetter(field_name)) 

362 

363 

364class CompareQuerySet(object): 

365 """Compare two querysets to see what's been added or removed.""" 

366 

367 def __init__(self, qs1, qs2): 

368 """Initialize the comparison. 

369 

370 :param qs1: The primary or "authoritative" queryset. 

371 :type qs1: django.db.models.QuerySet 

372 

373 :param qs2: The secondary queryset. 

374 :type qs2: django.db.models.QuerySet 

375 

376 """ 

377 self.qs1 = qs1 

378 self.qs2 = qs2 

379 

380 self._pk1 = list() 

381 for row in qs1: 

382 self._pk1.append(row.pk) 

383 

384 self._pk2 = list() 

385 for row in qs2: 

386 self._pk2.append(row.pk) 

387 

388 def get_added(self): 

389 """Get the primary keys of records in qs2 that do not appear in qs1. 

390 

391 :rtype: list 

392 

393 """ 

394 # Faster method for larger querysets? 

395 # https://stackoverflow.com/a/3462160/241720 

396 a = list() 

397 for pk in self._pk2: 

398 if pk not in self._pk1: 

399 a.append(pk) 

400 

401 return a 

402 

403 def get_removed(self): 

404 """Get the primary keys of records in qs2 that do not appear in qs1. 

405 

406 :rtype: list 

407 

408 """ 

409 # Faster method for larger querysets? 

410 # https://stackoverflow.com/a/3462160/241720 

411 a = list() 

412 for pk in self._pk2: 

413 if pk not in self._pk1: 

414 a.append(pk) 

415 

416 return a 

417 

418 def is_different(self): 

419 """Indicates whether the two querysets are different. 

420 

421 :rtype: bool 

422 

423 """ 

424 return set(self._pk1) != set(self._pk2) 

425 

426 def is_same(self): 

427 """Indicates whether the two querysets are the same. 

428 

429 :rtype: bool 

430 

431 """ 

432 return set(self._pk1) == set(self._pk2)