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
« 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."""
3import os
4from datetime import timedelta
6from django.conf import settings
7from django.core.exceptions import AppRegistryNotReady
8from django.db.utils import IntegrityError, OperationalError
9from django.utils import timezone
11import feedparser
12import requests
13import structlog
14from opentelemetry import trace
16import common.models
17import InvenTree.helpers
18from InvenTree.helpers_model import getModelsWithMixin
19from InvenTree.models import InvenTreeNotesMixin
20from InvenTree.tasks import ScheduledTask, scheduled_task
22tracer = trace.get_tracer(__name__)
23logger = structlog.get_logger('inventree')
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)
34 for output in common.models.DataOutput.objects.filter(created__lte=threshold):
35 output.delete()
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.
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
53 before = timezone.now() - timedelta(days=90)
55 # Delete notification records before the specified date
56 NotificationEntry.objects.filter(updated__lte=before).delete()
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
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
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
83 # Get a reference list
84 id_list = [a.feed_id for a in NewsFeedEntry.objects.all()]
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
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)
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
110 logger.info('update_news_feed: Sync done')
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.
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
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()
134 note_classes = getModelsWithMixin(InvenTreeNotesMixin)
135 before = InvenTree.helpers.current_date() - timedelta(days=90)
137 for note in NotesImage.objects.filter(date__lte=before):
138 # Find any images which are no longer referenced by a note
140 found = False
142 img = note.image.name
144 for model in note_classes:
145 if model.objects.filter(notes__icontains=img).exists():
146 found = True
147 break
149 if not found:
150 logger.info('Deleting note %s - image file not linked to a note', img)
151 note.delete()
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')
156 try:
157 images = os.listdir(notes_dir)
158 except FileNotFoundError:
159 # Thrown if the directory does not exist
160 images = []
162 all_notes = NotesImage.objects.all()
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
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))
177@tracer.start_as_current_span('rebuild_parameters')
178def rebuild_parameters(template_id):
179 """Rebuild all parameters for a given template.
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
186 try:
187 template = ParameterTemplate.objects.get(pk=template_id)
188 except ParameterTemplate.DoesNotExist:
189 return
191 parameters = Parameter.objects.filter(template=template)
193 n = 0
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()
200 if value_old != parameter.data_numeric:
201 parameter.full_clean()
202 parameter.save()
203 n += 1
205 if n > 0:
206 logger.info("Rebuilt %s parameters for template '%s'", n, template.name)
209@tracer.start_as_current_span('rebuild_attachment')
210def rebuild_attachment(attachment_id: int):
211 """Rebuild the given attachment, if possible.
213 This task is called whenever an attachment is saved, and perform the following tasks:
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
220 attachment = Attachment.objects.get(pk=attachment_id)
222 attachment.is_image = attachment.check_is_image()
223 attachment.generate_thumbnail()
224 attachment.save(rebuild=False)