Coverage for netbox/views/generic/feature_views.py: 45%

108 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.contrib import messages 

2from django.contrib.auth.mixins import LoginRequiredMixin 

3from django.contrib.contenttypes.models import ContentType 

4from django.db import router, transaction 

5from django.db.models import Q 

6from django.shortcuts import get_object_or_404, redirect, render 

7from django.utils.translation import gettext_lazy as _ 

8from django.views.generic import View 

9 

10from core.models import Job, ObjectChange 

11from core.tables import JobTable, ObjectChangeTable 

12from extras.forms import JournalEntryForm 

13from extras.models import ImageAttachment, JournalEntry 

14from extras.tables import JournalEntryTable 

15from tenancy.filtersets import ContactAssignmentFilterSet 

16from tenancy.forms import ContactAssignmentFilterForm 

17from tenancy.models import ContactAssignment 

18from tenancy.tables import ContactAssignmentTable 

19from utilities.permissions import get_permission_for_model 

20from utilities.views import ConditionalLoginRequiredMixin, GetReturnURLMixin, ViewTab, get_default_template 

21 

22from .base import BaseMultiObjectView 

23from .object_views import ObjectChildrenView 

24 

25__all__ = ( 

26 'BulkSyncDataView', 

27 'ObjectChangeLogView', 

28 'ObjectContactsView', 

29 'ObjectImageAttachmentsView', 

30 'ObjectJobsView', 

31 'ObjectJournalView', 

32 'ObjectSyncDataView', 

33) 

34 

35 

36class ObjectChangeLogView(ConditionalLoginRequiredMixin, View): 

37 """ 

38 Present a history of changes made to a particular object. The model class must be passed as a keyword argument 

39 when referencing this view in a URL path. For example: 

40 

41 path('sites/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='site_changelog', kwargs={'model': Site}), 

42 

43 Attributes: 

44 base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. 

45 """ 

46 base_template = None 

47 tab = ViewTab( 

48 label=_('Changelog'), 

49 permission='core.view_objectchange', 

50 weight=10000 

51 ) 

52 

53 def get(self, request, model, **kwargs): 

54 

55 # Handle QuerySet restriction of parent object if needed 

56 if hasattr(model.objects, 'restrict'): 

57 obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) 

58 else: 

59 obj = get_object_or_404(model, **kwargs) 

60 

61 # Gather all changes for this object (and its related objects) 

62 content_type = ContentType.objects.get_for_model(model) 

63 objectchanges = ObjectChange.objects.restrict(request.user, 'view').prefetch_related( 

64 'user', 'changed_object_type' 

65 ).filter( 

66 Q(changed_object_type=content_type, changed_object_id=obj.pk) | 

67 Q(related_object_type=content_type, related_object_id=obj.pk) 

68 ) 

69 objectchanges_table = ObjectChangeTable( 

70 data=objectchanges, 

71 orderable=False, 

72 ) 

73 objectchanges_table.configure(request) 

74 

75 return render(request, 'extras/object_changelog.html', { 

76 'object': obj, 

77 'table': objectchanges_table, 

78 'base_template': self.base_template or get_default_template(model), 

79 'tab': self.tab, 

80 }) 

81 

82 

83class ObjectImageAttachmentsView(ConditionalLoginRequiredMixin, View): 

84 """ 

85 Render all images attached to the object as linked thumbnails. 

86 

87 Attributes: 

88 base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. 

89 """ 

90 base_template = None 

91 tab = ViewTab( 

92 label=_('Images'), 

93 badge=lambda obj: obj.images.count(), 

94 permission='extras.view_imageattachment', 

95 weight=6000 

96 ) 

97 

98 def get(self, request, model, **kwargs): 

99 obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) 

100 image_attachments = ImageAttachment.objects.filter( 

101 object_type=ContentType.objects.get_for_model(obj), 

102 object_id=obj.pk, 

103 ) 

104 

105 return render(request, 'extras/object_imageattachments.html', { 

106 'object': obj, 

107 'image_attachments': image_attachments, 

108 'base_template': self.base_template or get_default_template(model), 

109 'tab': self.tab, 

110 }) 

111 

112 

113class ObjectJournalView(ConditionalLoginRequiredMixin, View): 

114 """ 

115 Show all journal entries for an object. The model class must be passed as a keyword argument when referencing this 

116 view in a URL path. For example: 

117 

118 path('sites/<int:pk>/journal/', ObjectJournalView.as_view(), name='site_journal', kwargs={'model': Site}), 

119 

120 Attributes: 

121 base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. 

122 """ 

123 base_template = None 

124 tab = ViewTab( 

125 label=_('Journal'), 

126 badge=lambda obj: obj.journal_entries.count(), 

127 permission='extras.view_journalentry', 

128 weight=9000 

129 ) 

130 

131 def get(self, request, model, **kwargs): 

132 

133 # Handle QuerySet restriction of parent object if needed 

134 if hasattr(model.objects, 'restrict'): 

135 obj = get_object_or_404(model.objects.restrict(request.user, 'view'), **kwargs) 

136 else: 

137 obj = get_object_or_404(model, **kwargs) 

138 

139 # Gather all changes for this object (and its related objects) 

140 content_type = ContentType.objects.get_for_model(model) 

141 journalentries = JournalEntry.objects.restrict(request.user, 'view').prefetch_related('created_by').filter( 

142 assigned_object_type=content_type, 

143 assigned_object_id=obj.pk 

144 ) 

145 journalentry_table = JournalEntryTable(journalentries) 

146 journalentry_table.configure(request) 

147 journalentry_table.columns.hide('assigned_object_type') 

148 journalentry_table.columns.hide('assigned_object') 

149 

150 if request.user.has_perm('extras.add_journalentry'): 

151 form = JournalEntryForm( 

152 initial={ 

153 'assigned_object_type': ContentType.objects.get_for_model(obj), 

154 'assigned_object_id': obj.pk 

155 } 

156 ) 

157 else: 

158 form = None 

159 

160 return render(request, 'extras/object_journal.html', { 

161 'object': obj, 

162 'form': form, 

163 'table': journalentry_table, 

164 'base_template': self.base_template or get_default_template(model), 

165 'tab': self.tab, 

166 }) 

167 

168 

169class ObjectJobsView(ConditionalLoginRequiredMixin, View): 

170 """ 

171 Render a list of all Job assigned to an object. For example: 

172 

173 path( 

174 'data-sources/<int:pk>/jobs/', 

175 ObjectJobsView.as_view(), 

176 name='datasource_jobs', 

177 kwargs={'model': DataSource} 

178 ) 

179 

180 Attributes: 

181 base_template: The name of the template to extend. If not provided, "{app}/{model}.html" will be used. 

182 """ 

183 base_template = None 

184 tab = ViewTab( 

185 label=_('Jobs'), 

186 badge=lambda obj: obj.jobs.count(), 

187 permission='core.view_job', 

188 weight=11000 

189 ) 

190 

191 def get_object(self, request, **kwargs): 

192 return get_object_or_404(self.model.objects.restrict(request.user, 'view'), **kwargs) 

193 

194 def get_jobs(self, instance): 

195 object_type = ContentType.objects.get_for_model(instance) 

196 return Job.objects.defer('data').filter( 

197 object_type=object_type, 

198 object_id=instance.id 

199 ) 

200 

201 def get(self, request, model, **kwargs): 

202 self.model = model 

203 obj = self.get_object(request, **kwargs) 

204 

205 # Gather all Jobs for this object 

206 jobs = self.get_jobs(obj) 

207 jobs_table = JobTable(data=jobs, orderable=False) 

208 jobs_table.configure(request) 

209 

210 return render(request, 'core/object_jobs.html', { 

211 'object': obj, 

212 'table': jobs_table, 

213 'base_template': self.base_template or get_default_template(model), 

214 'tab': self.tab, 

215 }) 

216 

217 

218class ObjectSyncDataView(LoginRequiredMixin, View): 

219 

220 def post(self, request, model, **kwargs): 

221 """ 

222 Synchronize data from the DataFile associated with this object. 

223 """ 

224 qs = model.objects.all() 

225 if hasattr(model.objects, 'restrict'): 

226 qs = qs.restrict(request.user, 'sync') 

227 obj = get_object_or_404(qs, **kwargs) 

228 

229 if not obj.data_file: 

230 messages.error(request, _("Unable to synchronize data: No data file set.")) 

231 return redirect(obj.get_absolute_url()) 

232 

233 obj.sync(save=True) 

234 messages.success(request, _("Synchronized data for {object_type} {object}.").format( 

235 object_type=model._meta.verbose_name, 

236 object=obj 

237 )) 

238 

239 return redirect(obj.get_absolute_url()) 

240 

241 

242class BulkSyncDataView(GetReturnURLMixin, BaseMultiObjectView): 

243 """ 

244 Synchronize multiple instances of a model inheriting from SyncedDataMixin. 

245 """ 

246 def get_required_permission(self): 

247 return get_permission_for_model(self.queryset.model, 'sync') 

248 

249 def post(self, request): 

250 selected_objects = self.queryset.filter( 

251 pk__in=request.POST.getlist('pk'), 

252 data_file__isnull=False 

253 ) 

254 

255 with transaction.atomic(using=router.db_for_write(self.queryset.model)): 

256 for obj in selected_objects: 

257 obj.sync(save=True) 

258 

259 messages.success(request, _("Synced {count} {object_type}").format( 

260 count=len(selected_objects), 

261 object_type=self.queryset.model._meta.verbose_name_plural 

262 )) 

263 

264 return redirect(self.get_return_url(request)) 

265 

266 

267class ObjectContactsView(ObjectChildrenView): 

268 child_model = ContactAssignment 

269 table = ContactAssignmentTable 

270 filterset = ContactAssignmentFilterSet 

271 filterset_form = ContactAssignmentFilterForm 

272 template_name = 'tenancy/object_contacts.html' 

273 tab = ViewTab( 

274 label=_('Contacts'), 

275 badge=lambda obj: obj.get_contacts().count(), 

276 permission='tenancy.view_contactassignment', 

277 weight=5000 

278 ) 

279 

280 def dispatch(self, request, *args, **kwargs): 

281 model = kwargs.pop('model') 

282 self.queryset = model.objects.all() 

283 return super().dispatch(request, *args, **kwargs) 

284 

285 def get_children(self, request, parent): 

286 return parent.get_contacts().restrict(request.user, 'view').order_by('priority', 'contact', 'role')