-
-
Notifications
You must be signed in to change notification settings - Fork 184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(accessLogExport)!: create new AccessLogExportTask to generate a csv of access logs TASK-871 #5258
feat(accessLogExport)!: create new AccessLogExportTask to generate a csv of access logs TASK-871 #5258
Changes from 11 commits
09373fb
4079ea0
eb67d2b
0f9ac39
2836ea3
b69a006
04dc2cd
4c46fb0
de7f7f5
ff45ba8
7837192
6c60768
43ce879
bd5c994
6eb71c2
a807f66
83bf0ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
# Generated by Django 4.2.15 on 2024-11-05 15:47 | ||
|
||
import django.db.models.deletion | ||
import private_storage.fields | ||
import private_storage.storage.files | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
|
||
import kpi.fields.file | ||
import kpi.fields.kpi_uid | ||
import kpi.models.asset_file | ||
import kpi.models.import_export_task | ||
|
||
|
||
def populate_common_export_tasks(apps, schema_editor): | ||
CommonExportTask = apps.get_model('kpi', 'CommonExportTask') | ||
ProjectViewExportTask = apps.get_model('kpi', 'ProjectViewExportTask') | ||
for project_view_task in ProjectViewExportTask.objects.all(): | ||
common_task = CommonExportTask.objects.create( | ||
data=project_view_task.data, | ||
messages=project_view_task.messages, | ||
status=project_view_task.status, | ||
date_created=project_view_task.date_created, | ||
result=project_view_task.result, | ||
user=project_view_task.user, | ||
) | ||
project_view_task.commonexporttask_ptr = common_task | ||
project_view_task.save() | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
('kpi', '0059_assetexportsettings_date_created_and_more'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='CommonExportTask', | ||
fields=[ | ||
( | ||
'id', | ||
models.AutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name='ID', | ||
), | ||
), | ||
('data', models.JSONField()), | ||
('messages', models.JSONField(default=dict)), | ||
( | ||
'status', | ||
models.CharField( | ||
choices=[ | ||
('created', 'created'), | ||
('processing', 'processing'), | ||
('error', 'error'), | ||
('complete', 'complete'), | ||
], | ||
default='created', | ||
max_length=32, | ||
), | ||
), | ||
('date_created', models.DateTimeField(auto_now_add=True)), | ||
( | ||
'result', | ||
private_storage.fields.PrivateFileField( | ||
max_length=380, | ||
storage=( | ||
private_storage.storage.files.PrivateFileSystemStorage() | ||
), | ||
upload_to=kpi.models.import_export_task.export_upload_to, | ||
), | ||
), | ||
( | ||
'user', | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.RunPython(populate_common_export_tasks), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Forgot: if we end up keeping the migration, this needs to be reversible, even if the reverse is RunPython.noop. Otherwise unapplying migrations breaks |
||
migrations.AddField( | ||
model_name='projectviewexporttask', | ||
name='commonexporttask_ptr', | ||
field=models.OneToOneField( | ||
null=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
parent_link=True, | ||
to='kpi.CommonExportTask', | ||
), | ||
preserve_default=False, | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='data', | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='date_created', | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='id', | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='messages', | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='result', | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='status', | ||
), | ||
migrations.RemoveField( | ||
model_name='projectviewexporttask', | ||
name='user', | ||
), | ||
migrations.AlterField( | ||
model_name='assetfile', | ||
name='content', | ||
field=kpi.fields.file.PrivateExtendedFileField( | ||
max_length=380, null=True, upload_to=kpi.models.asset_file.upload_to | ||
), | ||
), | ||
migrations.CreateModel( | ||
name='AccessLogExportTask', | ||
fields=[ | ||
( | ||
'commonexporttask_ptr', | ||
models.OneToOneField( | ||
auto_created=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
parent_link=True, | ||
primary_key=True, | ||
serialize=False, | ||
to='kpi.commonexporttask', | ||
), | ||
), | ||
('uid', kpi.fields.kpi_uid.KpiUidField(_null=False, uid_prefix='ale')), | ||
('get_all_logs', models.BooleanField(default=False)), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
bases=('kpi.commonexporttask',), | ||
), | ||
] | ||
|
||
migrations.AddField( | ||
model_name='projectviewexporttask', | ||
name='commonexporttask_ptr', | ||
field=models.OneToOneField( | ||
null=True, | ||
on_delete=django.db.models.deletion.CASCADE, | ||
parent_link=True, | ||
to='kpi.CommonExportTask', | ||
), | ||
), |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Generated by Django 4.2.15 on 2024-11-14 22:46 | ||
|
||
from django.conf import settings | ||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
('kpi', '0060_commonexporttask_remove_projectviewexporttask_data_and_more'), | ||
] | ||
|
||
operations = [ | ||
migrations.RenameModel( | ||
old_name='SynchronousExport', | ||
new_name='SubmissionSynchronousExport', | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Generated by Django 4.2.15 on 2024-11-14 22:50 | ||
|
||
from django.conf import settings | ||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
('kpi', '0061_rename_synchronousexport_submissionsynchronousexport'), | ||
] | ||
|
||
operations = [ | ||
migrations.RenameModel( | ||
old_name='ExportTask', | ||
new_name='SubmissionExportTask', | ||
), | ||
] |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -57,9 +57,9 @@ | |||||
from kpi.exceptions import XlsFormatException | ||||||
from kpi.fields import KpiUidField | ||||||
from kpi.models import Asset | ||||||
from kpi.utils.data_exports import create_data_export | ||||||
from kpi.utils.log import logging | ||||||
from kpi.utils.models import _load_library_content, create_assets, resolve_url_to_asset | ||||||
from kpi.utils.project_view_exports import create_project_view_export | ||||||
from kpi.utils.rename_xls_sheet import ( | ||||||
ConflictSheetError, | ||||||
NoFromSheetError, | ||||||
|
@@ -134,7 +134,7 @@ def run(self): | |||||
# This method must be implemented by a subclass | ||||||
self._run_task(msgs) | ||||||
self.status = self.COMPLETE | ||||||
except ExportTaskBase.InaccessibleData as e: | ||||||
except SubmissionExportTaskBase.InaccessibleData as e: | ||||||
msgs['error_type'] = t('Cannot access data') | ||||||
msgs['error'] = str(e) | ||||||
self.status = self.ERROR | ||||||
|
@@ -482,27 +482,23 @@ def export_upload_to(self, filename): | |||||
return posixpath.join(self.user.username, 'exports', filename) | ||||||
|
||||||
|
||||||
class ProjectViewExportTask(ImportExportTask): | ||||||
uid = KpiUidField(uid_prefix='pve') | ||||||
class CommonExportTask(ImportExportTask): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have caught this before, but I think we may be able to avoid the complicated migration if we make this a mixin rather than a separate model. |
||||||
result = PrivateFileField(upload_to=export_upload_to, max_length=380) | ||||||
|
||||||
def _get_export_details(self) -> tuple: | ||||||
return self.data['type'], self.data['view'] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should account for |
||||||
|
||||||
def _build_export_filename( | ||||||
self, export_type: str, username: str, view: str | ||||||
) -> str: | ||||||
time = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') | ||||||
return f'{export_type}-{username}-view_{view}-{time}.csv' | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as previous comment. This should be updated to allow an empty/None |
||||||
|
||||||
def _run_task(self, messages: list) -> None: | ||||||
export_type = self.data['type'] | ||||||
view = self.data['view'] | ||||||
|
||||||
filename = self._build_export_filename( | ||||||
export_type, self.user.username, view | ||||||
) | ||||||
def _run_task_base(self, messages: list, buff) -> None: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
since it's no longer doing the whole task |
||||||
export_type, view = self._get_export_details() | ||||||
filename = self._build_export_filename(export_type, self.user.username, view) | ||||||
absolute_filepath = self.get_absolute_filepath(filename) | ||||||
|
||||||
buff = create_project_view_export(export_type, self.user.username, view) | ||||||
|
||||||
with self.result.storage.open(absolute_filepath, 'wb') as output_file: | ||||||
output_file.write(buff.read().encode()) | ||||||
|
||||||
|
@@ -515,7 +511,31 @@ def delete(self, *args, **kwargs) -> None: | |||||
super().delete(*args, **kwargs) | ||||||
|
||||||
|
||||||
class ExportTaskBase(ImportExportTask): | ||||||
class AccessLogExportTask(CommonExportTask): | ||||||
uid = KpiUidField(uid_prefix='ale') | ||||||
get_all_logs = models.BooleanField(default=False) | ||||||
|
||||||
def _run_task(self, messages: list) -> None: | ||||||
if self.get_all_logs and not self.user.is_superuser: | ||||||
raise PermissionError('Only superusers can export all access logs.') | ||||||
|
||||||
export_type, view = self._get_export_details() | ||||||
buff = create_data_export( | ||||||
export_type, self.user.username, self.uid, self.get_all_logs | ||||||
) | ||||||
self._run_task_base(messages, buff) | ||||||
|
||||||
|
||||||
class ProjectViewExportTask(CommonExportTask): | ||||||
uid = KpiUidField(uid_prefix='pve') | ||||||
|
||||||
def _run_task(self, messages: list) -> None: | ||||||
export_type, view = self._get_export_details() | ||||||
buff = create_data_export(export_type, self.user.username, view, False) | ||||||
self._run_task_base(messages, buff) | ||||||
|
||||||
|
||||||
class SubmissionExportTaskBase(ImportExportTask): | ||||||
""" | ||||||
An (asynchronous) submission data export job. The instantiator must set the | ||||||
`data` attribute to a dictionary with the following keys: | ||||||
|
@@ -945,7 +965,7 @@ def remove_excess(cls, user, source): | |||||
export.delete() | ||||||
|
||||||
|
||||||
class ExportTask(ExportTaskBase): | ||||||
class SubmissionExportTask(SubmissionExportTaskBase): | ||||||
""" | ||||||
An asynchronous export task, to be run with Celery | ||||||
""" | ||||||
|
@@ -966,7 +986,7 @@ def _run_task(self, messages): | |||||
self.remove_excess(self.user, source_url) | ||||||
|
||||||
|
||||||
class SynchronousExport(ExportTaskBase): | ||||||
class SubmissionSynchronousExport(SubmissionExportTaskBase): | ||||||
""" | ||||||
A synchronous export, with significant limitations on processing time, but | ||||||
offered for user convenience | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, definitely non-blocking