Coverage for src/backend/InvenTree/common/tasks.py: 20%

125 statements  

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

1"""Tasks (processes that get offloaded) for common app.""" 

2 

3import os 

4from datetime import timedelta 

5 

6from django.conf import settings 

7from django.core.exceptions import AppRegistryNotReady 

8from django.db.utils import IntegrityError, OperationalError 

9from django.utils import timezone 

10 

11import feedparser 

12import requests 

13import structlog 

14from opentelemetry import trace 

15 

16import common.models 

17import InvenTree.helpers 

18from InvenTree.helpers_model import getModelsWithMixin 

19from InvenTree.models import InvenTreeNotesMixin 

20from InvenTree.tasks import ScheduledTask, scheduled_task 

21 

22tracer = trace.get_tracer(__name__) 

23logger = structlog.get_logger('inventree') 

24 

25 

26@tracer.start_as_current_span('cleanup_old_data_outputs') 

27@scheduled_task(ScheduledTask.DAILY) 

28def cleanup_old_data_outputs(): 

29 """Remove old data outputs from the database.""" 

30 # Remove any outputs which are older than 5 days 

31 # Note: Remove them individually, to ensure that the files are removed too 

32 threshold = InvenTree.helpers.current_date() - timedelta(days=5) 

33 

34 for output in common.models.DataOutput.objects.filter(created__lte=threshold): 

35 output.delete() 

36 

37 

38@tracer.start_as_current_span('delete_old_notifications') 

39@scheduled_task(ScheduledTask.DAILY) 

40def delete_old_notifications(): 

41 """Remove old notifications from the database. 

42 

43 Anything older than ~3 months is removed 

44 """ 

45 try: 

46 from common.models import NotificationEntry 

47 except AppRegistryNotReady: # pragma: no cover 

48 logger.info( 

49 "Could not perform 'delete_old_notifications' - App registry not ready" 

50 ) 

51 return 

52 

53 before = timezone.now() - timedelta(days=90) 

54 

55 # Delete notification records before the specified date 

56 NotificationEntry.objects.filter(updated__lte=before).delete() 

57 

58 

59@tracer.start_as_current_span('update_news_feed') 

60@scheduled_task(ScheduledTask.DAILY) 

61def update_news_feed(): 

62 """Update the newsfeed.""" 

63 try: 

64 from common.models import NewsFeedEntry 

65 except AppRegistryNotReady: # pragma: no cover 

66 logger.info("Could not perform 'update_news_feed' - App registry not ready") 

67 return 

68 

69 # News feed isn't defined, no need to continue 

70 if not settings.INVENTREE_NEWS_URL or not isinstance( 

71 settings.INVENTREE_NEWS_URL, str 

72 ): 

73 return 

74 

75 # Fetch and parse feed 

76 try: 

77 feed = requests.get(settings.INVENTREE_NEWS_URL, timeout=30) 

78 d = feedparser.parse(feed.content) 

79 except Exception: # pragma: no cover 

80 logger.warning('update_news_feed: Error parsing the newsfeed') 

81 return 

82 

83 # Get a reference list 

84 id_list = [a.feed_id for a in NewsFeedEntry.objects.all()] 

85 

86 # Iterate over entries 

87 for entry in d.entries: 

88 # Check if id already exists 

89 if entry.id in id_list: 

90 continue 

91 

92 # Enforce proper links for the entries 

93 if entry.link and str(entry.link).startswith('/'): 

94 entry.link = settings.INVENTREE_BASE_URL + str(entry.link) 

95 

96 # Create entry 

97 try: 

98 NewsFeedEntry.objects.create( 

99 feed_id=entry.id, 

100 title=entry.title, 

101 link=entry.link, 

102 published=entry.published, 

103 author=entry.author, 

104 summary=entry.summary, 

105 ) 

106 except (IntegrityError, OperationalError): 

107 # Sometimes errors-out on database start-up 

108 pass 

109 

110 logger.info('update_news_feed: Sync done') 

111 

112 

113@tracer.start_as_current_span('delete_old_notes_images') 

114@scheduled_task(ScheduledTask.DAILY) 

115def delete_old_notes_images(): 

116 """Remove old notes images from the database. 

117 

118 Anything older than ~3 months is removed, unless it is linked to a note 

119 """ 

120 try: 

121 from common.models import NotesImage 

122 except AppRegistryNotReady: 

123 logger.info( 

124 "Could not perform 'delete_old_notes_images' - App registry not ready" 

125 ) 

126 return 

127 

128 # Remove any notes which point to non-existent image files 

129 for note in NotesImage.objects.all(): 

130 if not os.path.exists(note.image.path): 

131 logger.info('Deleting note %s - image file does not exist', note.image.path) 

132 note.delete() 

133 

134 note_classes = getModelsWithMixin(InvenTreeNotesMixin) 

135 before = InvenTree.helpers.current_date() - timedelta(days=90) 

136 

137 for note in NotesImage.objects.filter(date__lte=before): 

138 # Find any images which are no longer referenced by a note 

139 

140 found = False 

141 

142 img = note.image.name 

143 

144 for model in note_classes: 

145 if model.objects.filter(notes__icontains=img).exists(): 

146 found = True 

147 break 

148 

149 if not found: 

150 logger.info('Deleting note %s - image file not linked to a note', img) 

151 note.delete() 

152 

153 # Finally, remove any images in the notes dir which are not linked to a note 

154 notes_dir = os.path.join(settings.MEDIA_ROOT, 'notes') 

155 

156 try: 

157 images = os.listdir(notes_dir) 

158 except FileNotFoundError: 

159 # Thrown if the directory does not exist 

160 images = [] 

161 

162 all_notes = NotesImage.objects.all() 

163 

164 for image in images: 

165 found = False 

166 for note in all_notes: 

167 img_path = os.path.basename(note.image.path) 

168 if img_path == image: 

169 found = True 

170 break 

171 

172 if not found: 

173 logger.info('Deleting note %s - image file not linked to a note', image) 

174 os.remove(os.path.join(notes_dir, image)) 

175 

176 

177@tracer.start_as_current_span('rebuild_parameters') 

178def rebuild_parameters(template_id): 

179 """Rebuild all parameters for a given template. 

180 

181 This function is called when a base template is changed, 

182 which may cause the base unit to be adjusted. 

183 """ 

184 from common.models import Parameter, ParameterTemplate 

185 

186 try: 

187 template = ParameterTemplate.objects.get(pk=template_id) 

188 except ParameterTemplate.DoesNotExist: 

189 return 

190 

191 parameters = Parameter.objects.filter(template=template) 

192 

193 n = 0 

194 

195 for parameter in parameters: 

196 # Update the parameter if the numeric value has changed 

197 value_old = parameter.data_numeric 

198 parameter.calculate_numeric_value() 

199 

200 if value_old != parameter.data_numeric: 

201 parameter.full_clean() 

202 parameter.save() 

203 n += 1 

204 

205 if n > 0: 

206 logger.info("Rebuilt %s parameters for template '%s'", n, template.name) 

207 

208 

209@tracer.start_as_current_span('rebuild_attachment') 

210def rebuild_attachment(attachment_id: int): 

211 """Rebuild the given attachment, if possible. 

212 

213 This task is called whenever an attachment is saved, and perform the following tasks: 

214 

215 - Check if the attachment is an image file, and update the "is_image" field accordingly 

216 - Attempt to generate a thumbnail for the attachment 

217 """ 

218 from common.models import Attachment 

219 

220 attachment = Attachment.objects.get(pk=attachment_id) 

221 

222 attachment.is_image = attachment.check_is_image() 

223 attachment.generate_thumbnail() 

224 attachment.save(rebuild=False)